mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 02:09:05 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			1570 lines
		
	
	
	
		
			48 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1570 lines
		
	
	
	
		
			48 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-*/
 | 
						||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
 | 
						||
/* This Source Code Form is subject to the terms of the Mozilla Public
 | 
						||
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 | 
						||
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | 
						||
 | 
						||
"use strict";
 | 
						||
 | 
						||
// Time in ms before we start changing the sort order again after receiving a
 | 
						||
// mousemove event.
 | 
						||
const TIME_BEFORE_SORTING_AGAIN = 5000;
 | 
						||
 | 
						||
// How long we should wait between samples.
 | 
						||
const MINIMUM_INTERVAL_BETWEEN_SAMPLES_MS = 1000;
 | 
						||
 | 
						||
// How often we should update
 | 
						||
const UPDATE_INTERVAL_MS = 2000;
 | 
						||
 | 
						||
const NS_PER_US = 1000;
 | 
						||
const NS_PER_MS = 1000 * 1000;
 | 
						||
const NS_PER_S = 1000 * 1000 * 1000;
 | 
						||
const NS_PER_MIN = NS_PER_S * 60;
 | 
						||
const NS_PER_HOUR = NS_PER_MIN * 60;
 | 
						||
const NS_PER_DAY = NS_PER_HOUR * 24;
 | 
						||
 | 
						||
const ONE_GIGA = 1024 * 1024 * 1024;
 | 
						||
const ONE_MEGA = 1024 * 1024;
 | 
						||
const ONE_KILO = 1024;
 | 
						||
 | 
						||
const { XPCOMUtils } = ChromeUtils.importESModule(
 | 
						||
  "resource://gre/modules/XPCOMUtils.sys.mjs"
 | 
						||
);
 | 
						||
const { AppConstants } = ChromeUtils.importESModule(
 | 
						||
  "resource://gre/modules/AppConstants.sys.mjs"
 | 
						||
);
 | 
						||
 | 
						||
ChromeUtils.defineESModuleGetters(this, {
 | 
						||
  ContextualIdentityService:
 | 
						||
    "resource://gre/modules/ContextualIdentityService.sys.mjs",
 | 
						||
});
 | 
						||
 | 
						||
ChromeUtils.defineLazyGetter(this, "ProfilerPopupBackground", function () {
 | 
						||
  return ChromeUtils.importESModule(
 | 
						||
    "resource://devtools/client/performance-new/shared/background.sys.mjs"
 | 
						||
  );
 | 
						||
});
 | 
						||
 | 
						||
const { WebExtensionPolicy } = Cu.getGlobalForObject(Services);
 | 
						||
 | 
						||
const SHOW_THREADS = Services.prefs.getBoolPref(
 | 
						||
  "toolkit.aboutProcesses.showThreads"
 | 
						||
);
 | 
						||
const SHOW_ALL_SUBFRAMES = Services.prefs.getBoolPref(
 | 
						||
  "toolkit.aboutProcesses.showAllSubframes"
 | 
						||
);
 | 
						||
const SHOW_PROFILER_ICONS = Services.prefs.getBoolPref(
 | 
						||
  "toolkit.aboutProcesses.showProfilerIcons"
 | 
						||
);
 | 
						||
const PROFILE_DURATION = Math.max(
 | 
						||
  1,
 | 
						||
  Services.prefs.getIntPref("toolkit.aboutProcesses.profileDuration")
 | 
						||
);
 | 
						||
 | 
						||
/**
 | 
						||
 * For the time being, Fluent doesn't support duration or memory formats, so we need
 | 
						||
 * to fetch units from Fluent. To avoid re-fetching at each update, we prefetch these
 | 
						||
 * units during initialization, asynchronously, and keep them.
 | 
						||
 *
 | 
						||
 * @type {
 | 
						||
 *   duration: { ns: String, us: String, ms: String, s: String, m: String, h: String, d: String },
 | 
						||
 *   memory: { B: String, KB: String, MB: String, GB: String, TB: String, PB: String, EB: String }
 | 
						||
 * }.
 | 
						||
 */
 | 
						||
let gLocalizedUnits;
 | 
						||
 | 
						||
let tabFinder = {
 | 
						||
  update() {
 | 
						||
    this._map = new Map();
 | 
						||
    for (let win of Services.wm.getEnumerator("navigator:browser")) {
 | 
						||
      let tabbrowser = win.gBrowser;
 | 
						||
      for (let browser of tabbrowser.browsers) {
 | 
						||
        let id = browser.outerWindowID; // May be `null` if the browser isn't loaded yet
 | 
						||
        if (id != null) {
 | 
						||
          this._map.set(id, browser);
 | 
						||
        }
 | 
						||
      }
 | 
						||
      if (tabbrowser.preloadedBrowser) {
 | 
						||
        let browser = tabbrowser.preloadedBrowser;
 | 
						||
        if (browser.outerWindowID) {
 | 
						||
          this._map.set(browser.outerWindowID, browser);
 | 
						||
        }
 | 
						||
      }
 | 
						||
    }
 | 
						||
  },
 | 
						||
 | 
						||
  /**
 | 
						||
   * Find the <xul:tab> for a window id.
 | 
						||
   *
 | 
						||
   * This is useful e.g. for reloading or closing tabs.
 | 
						||
   *
 | 
						||
   * @return null If the xul:tab could not be found, e.g. if the
 | 
						||
   * windowId is that of a chrome window.
 | 
						||
   * @return {{tabbrowser: <xul:tabbrowser>, tab: <xul.tab>}} The
 | 
						||
   * tabbrowser and tab if the latter could be found.
 | 
						||
   */
 | 
						||
  get(id) {
 | 
						||
    let browser = this._map.get(id);
 | 
						||
    if (!browser) {
 | 
						||
      return null;
 | 
						||
    }
 | 
						||
    let tabbrowser = browser.getTabBrowser();
 | 
						||
    if (!tabbrowser) {
 | 
						||
      return {
 | 
						||
        tabbrowser: null,
 | 
						||
        tab: {
 | 
						||
          getAttribute() {
 | 
						||
            return "";
 | 
						||
          },
 | 
						||
          linkedBrowser: browser,
 | 
						||
        },
 | 
						||
      };
 | 
						||
    }
 | 
						||
    return { tabbrowser, tab: tabbrowser.getTabForBrowser(browser) };
 | 
						||
  },
 | 
						||
};
 | 
						||
 | 
						||
/**
 | 
						||
 * Utilities for dealing with state
 | 
						||
 */
 | 
						||
var State = {
 | 
						||
  // Store the previous and current samples so they can be compared.
 | 
						||
  _previous: null,
 | 
						||
  _latest: null,
 | 
						||
 | 
						||
  async _promiseSnapshot() {
 | 
						||
    let date = Cu.now();
 | 
						||
    let main = await ChromeUtils.requestProcInfo();
 | 
						||
    main.date = date;
 | 
						||
 | 
						||
    let processes = new Map();
 | 
						||
    processes.set(main.pid, main);
 | 
						||
    for (let child of main.children) {
 | 
						||
      child.date = date;
 | 
						||
      processes.set(child.pid, child);
 | 
						||
    }
 | 
						||
 | 
						||
    return { processes, date };
 | 
						||
  },
 | 
						||
 | 
						||
  /**
 | 
						||
   * Update the internal state.
 | 
						||
   *
 | 
						||
   * @return {Promise}
 | 
						||
   */
 | 
						||
  async update(force = false) {
 | 
						||
    if (
 | 
						||
      force ||
 | 
						||
      !this._latest ||
 | 
						||
      Cu.now() - this._latest.date > MINIMUM_INTERVAL_BETWEEN_SAMPLES_MS
 | 
						||
    ) {
 | 
						||
      // Replacing this._previous before we are done awaiting
 | 
						||
      // this._promiseSnapshot can cause this._previous and this._latest to be
 | 
						||
      // equal for a short amount of time, which can cause test failures when
 | 
						||
      // a forced update of the display is triggered in the meantime.
 | 
						||
      let newSnapshot = await this._promiseSnapshot();
 | 
						||
      this._previous = this._latest;
 | 
						||
      this._latest = newSnapshot;
 | 
						||
    }
 | 
						||
  },
 | 
						||
 | 
						||
  _getThreadDelta(cur, prev, deltaT) {
 | 
						||
    let result = {
 | 
						||
      tid: cur.tid,
 | 
						||
      name: cur.name || `(${cur.tid})`,
 | 
						||
      // Total amount of CPU used, in ns.
 | 
						||
      totalCpu: cur.cpuTime,
 | 
						||
      slopeCpu: null,
 | 
						||
      active: null,
 | 
						||
    };
 | 
						||
    if (!deltaT) {
 | 
						||
      return result;
 | 
						||
    }
 | 
						||
    result.slopeCpu = (result.totalCpu - (prev ? prev.cpuTime : 0)) / deltaT;
 | 
						||
    result.active =
 | 
						||
      !!result.slopeCpu || cur.cpuCycleCount > (prev ? prev.cpuCycleCount : 0);
 | 
						||
    return result;
 | 
						||
  },
 | 
						||
 | 
						||
  _getDOMWindows(process) {
 | 
						||
    if (!process.windows) {
 | 
						||
      return [];
 | 
						||
    }
 | 
						||
    if (!process.type == "extensions") {
 | 
						||
      return [];
 | 
						||
    }
 | 
						||
    let windows = process.windows.map(win => {
 | 
						||
      let tab = tabFinder.get(win.outerWindowId);
 | 
						||
      let addon =
 | 
						||
        process.type == "extension"
 | 
						||
          ? WebExtensionPolicy.getByURI(win.documentURI)
 | 
						||
          : null;
 | 
						||
      let displayRank;
 | 
						||
      if (tab) {
 | 
						||
        displayRank = 1;
 | 
						||
      } else if (win.isProcessRoot) {
 | 
						||
        displayRank = 2;
 | 
						||
      } else if (win.documentTitle) {
 | 
						||
        displayRank = 3;
 | 
						||
      } else {
 | 
						||
        displayRank = 4;
 | 
						||
      }
 | 
						||
      return {
 | 
						||
        outerWindowId: win.outerWindowId,
 | 
						||
        documentURI: win.documentURI,
 | 
						||
        documentTitle: win.documentTitle,
 | 
						||
        isProcessRoot: win.isProcessRoot,
 | 
						||
        isInProcess: win.isInProcess,
 | 
						||
        tab,
 | 
						||
        addon,
 | 
						||
        // The number of instances we have collapsed.
 | 
						||
        count: 1,
 | 
						||
        // A rank used to quickly sort windows.
 | 
						||
        displayRank,
 | 
						||
      };
 | 
						||
    });
 | 
						||
 | 
						||
    // We keep all tabs and addons but we collapse subframes that have the same host.
 | 
						||
 | 
						||
    // A map from host -> subframe.
 | 
						||
    let collapsible = new Map();
 | 
						||
    let result = [];
 | 
						||
    for (let win of windows) {
 | 
						||
      if (win.tab || win.addon) {
 | 
						||
        result.push(win);
 | 
						||
        continue;
 | 
						||
      }
 | 
						||
      let prev = collapsible.get(win.documentURI.prePath);
 | 
						||
      if (prev) {
 | 
						||
        prev.count += 1;
 | 
						||
      } else {
 | 
						||
        collapsible.set(win.documentURI.prePath, win);
 | 
						||
        result.push(win);
 | 
						||
      }
 | 
						||
    }
 | 
						||
    return result;
 | 
						||
  },
 | 
						||
 | 
						||
  /**
 | 
						||
   * Compute the delta between two process snapshots.
 | 
						||
   *
 | 
						||
   * @param {ProcessSnapshot} cur
 | 
						||
   * @param {ProcessSnapshot?} prev
 | 
						||
   */
 | 
						||
  _getProcessDelta(cur, prev) {
 | 
						||
    let windows = this._getDOMWindows(cur);
 | 
						||
    let result = {
 | 
						||
      pid: cur.pid,
 | 
						||
      childID: cur.childID,
 | 
						||
      totalRamSize: cur.memory,
 | 
						||
      deltaRamSize: null,
 | 
						||
      totalCpu: cur.cpuTime,
 | 
						||
      slopeCpu: null,
 | 
						||
      active: null,
 | 
						||
      type: cur.type,
 | 
						||
      origin: cur.origin || "",
 | 
						||
      threads: null,
 | 
						||
      displayRank: Control._getDisplayGroupRank(cur, windows),
 | 
						||
      windows,
 | 
						||
      utilityActors: cur.utilityActors,
 | 
						||
      // If this process has an unambiguous title, store it here.
 | 
						||
      title: null,
 | 
						||
    };
 | 
						||
    // Attempt to determine a title for this process.
 | 
						||
    let titles = [
 | 
						||
      ...new Set(
 | 
						||
        result.windows
 | 
						||
          .filter(win => win.documentTitle)
 | 
						||
          .map(win => win.documentTitle)
 | 
						||
      ),
 | 
						||
    ];
 | 
						||
    if (titles.length == 1) {
 | 
						||
      result.title = titles[0];
 | 
						||
    }
 | 
						||
    if (!prev) {
 | 
						||
      if (SHOW_THREADS) {
 | 
						||
        result.threads = cur.threads.map(data => this._getThreadDelta(data));
 | 
						||
      }
 | 
						||
      return result;
 | 
						||
    }
 | 
						||
    if (prev.pid != cur.pid) {
 | 
						||
      throw new Error("Assertion failed: A process cannot change pid.");
 | 
						||
    }
 | 
						||
    let deltaT = (cur.date - prev.date) * NS_PER_MS;
 | 
						||
    let threads = null;
 | 
						||
    if (SHOW_THREADS) {
 | 
						||
      let prevThreads = new Map();
 | 
						||
      for (let thread of prev.threads) {
 | 
						||
        prevThreads.set(thread.tid, thread);
 | 
						||
      }
 | 
						||
      threads = cur.threads.map(curThread =>
 | 
						||
        this._getThreadDelta(curThread, prevThreads.get(curThread.tid), deltaT)
 | 
						||
      );
 | 
						||
    }
 | 
						||
    result.deltaRamSize = cur.memory - prev.memory;
 | 
						||
    result.slopeCpu = (cur.cpuTime - prev.cpuTime) / deltaT;
 | 
						||
    result.active = !!result.slopeCpu || cur.cpuCycleCount > prev.cpuCycleCount;
 | 
						||
    result.threads = threads;
 | 
						||
    return result;
 | 
						||
  },
 | 
						||
 | 
						||
  getCounters() {
 | 
						||
    tabFinder.update();
 | 
						||
 | 
						||
    let counters = [];
 | 
						||
 | 
						||
    for (let cur of this._latest.processes.values()) {
 | 
						||
      let prev = this._previous?.processes.get(cur.pid);
 | 
						||
      counters.push(this._getProcessDelta(cur, prev));
 | 
						||
    }
 | 
						||
 | 
						||
    return counters;
 | 
						||
  },
 | 
						||
};
 | 
						||
 | 
						||
var View = {
 | 
						||
  // Processes, tabs and subframes that we killed during the previous iteration.
 | 
						||
  // Array<{pid:Number} | {windowId:Number}>
 | 
						||
  _killedRecently: [],
 | 
						||
  commit() {
 | 
						||
    this._killedRecently.length = 0;
 | 
						||
    let tbody = document.getElementById("process-tbody");
 | 
						||
 | 
						||
    let insertPoint = tbody.firstChild;
 | 
						||
    let nextRow;
 | 
						||
    while ((nextRow = this._orderedRows.shift())) {
 | 
						||
      if (insertPoint && insertPoint === nextRow) {
 | 
						||
        insertPoint = insertPoint.nextSibling;
 | 
						||
      } else {
 | 
						||
        tbody.insertBefore(nextRow, insertPoint);
 | 
						||
      }
 | 
						||
    }
 | 
						||
 | 
						||
    if (insertPoint) {
 | 
						||
      while ((nextRow = insertPoint.nextSibling)) {
 | 
						||
        this._removeRow(nextRow);
 | 
						||
      }
 | 
						||
      this._removeRow(insertPoint);
 | 
						||
    }
 | 
						||
  },
 | 
						||
  // If we are not going to display the updated list of rows, drop references
 | 
						||
  // to rows that haven't been inserted in the DOM tree.
 | 
						||
  discardUpdate() {
 | 
						||
    for (let row of this._orderedRows) {
 | 
						||
      if (!row.parentNode) {
 | 
						||
        this._rowsById.delete(row.rowId);
 | 
						||
      }
 | 
						||
    }
 | 
						||
    this._orderedRows = [];
 | 
						||
  },
 | 
						||
  insertAfterRow(row) {
 | 
						||
    let tbody = row.parentNode;
 | 
						||
    let nextRow;
 | 
						||
    while ((nextRow = this._orderedRows.pop())) {
 | 
						||
      tbody.insertBefore(nextRow, row.nextSibling);
 | 
						||
    }
 | 
						||
  },
 | 
						||
 | 
						||
  _rowsById: new Map(),
 | 
						||
  _removeRow(row) {
 | 
						||
    this._rowsById.delete(row.rowId);
 | 
						||
 | 
						||
    row.remove();
 | 
						||
  },
 | 
						||
  _getOrCreateRow(rowId, cellCount) {
 | 
						||
    let row = this._rowsById.get(rowId);
 | 
						||
    if (!row) {
 | 
						||
      row = document.createElement("tr");
 | 
						||
      while (cellCount--) {
 | 
						||
        row.appendChild(document.createElement("td"));
 | 
						||
      }
 | 
						||
      row.rowId = rowId;
 | 
						||
      this._rowsById.set(rowId, row);
 | 
						||
    }
 | 
						||
    this._orderedRows.push(row);
 | 
						||
    return row;
 | 
						||
  },
 | 
						||
 | 
						||
  displayCpu(data, cpuCell, maxSlopeCpu) {
 | 
						||
    // Put a value < 0% when we really don't want to see a bar as
 | 
						||
    // otherwise it sometimes appears due to rounding errors when we
 | 
						||
    // don't have an integer number of pixels.
 | 
						||
    let barWidth = -0.5;
 | 
						||
    if (data.slopeCpu == null) {
 | 
						||
      this._fillCell(cpuCell, {
 | 
						||
        fluentName: "about-processes-cpu-user-and-kernel-not-ready",
 | 
						||
        classes: ["cpu"],
 | 
						||
      });
 | 
						||
    } else {
 | 
						||
      let { duration, unit } = this._getDuration(data.totalCpu);
 | 
						||
      if (data.totalCpu == 0) {
 | 
						||
        // A thread having used exactly 0ns of CPU time is not possible.
 | 
						||
        // When we get 0 it means the thread used less than the precision of
 | 
						||
        // the measurement, and it makes more sense to show '0ms' than '0ns'.
 | 
						||
        // This is useful on Linux where the minimum non-zero CPU time value
 | 
						||
        // for threads of child processes is 10ms, and on Windows ARM64 where
 | 
						||
        // the minimum non-zero value is 16ms.
 | 
						||
        unit = "ms";
 | 
						||
      }
 | 
						||
      let localizedUnit = gLocalizedUnits.duration[unit];
 | 
						||
      if (data.slopeCpu == 0) {
 | 
						||
        let fluentName = data.active
 | 
						||
          ? "about-processes-cpu-almost-idle"
 | 
						||
          : "about-processes-cpu-fully-idle";
 | 
						||
        this._fillCell(cpuCell, {
 | 
						||
          fluentName,
 | 
						||
          fluentArgs: {
 | 
						||
            total: duration,
 | 
						||
            unit: localizedUnit,
 | 
						||
          },
 | 
						||
          classes: ["cpu"],
 | 
						||
        });
 | 
						||
      } else {
 | 
						||
        this._fillCell(cpuCell, {
 | 
						||
          fluentName: "about-processes-cpu",
 | 
						||
          fluentArgs: {
 | 
						||
            percent: data.slopeCpu,
 | 
						||
            total: duration,
 | 
						||
            unit: localizedUnit,
 | 
						||
          },
 | 
						||
          classes: ["cpu"],
 | 
						||
        });
 | 
						||
 | 
						||
        let cpuPercent = data.slopeCpu * 100;
 | 
						||
        if (maxSlopeCpu > 1) {
 | 
						||
          cpuPercent /= maxSlopeCpu;
 | 
						||
        }
 | 
						||
        // Ensure we always have a visible bar for non-0 values.
 | 
						||
        barWidth = Math.max(0.5, cpuPercent);
 | 
						||
      }
 | 
						||
    }
 | 
						||
    cpuCell.style.setProperty("--bar-width", barWidth);
 | 
						||
  },
 | 
						||
 | 
						||
  /**
 | 
						||
   * Display a row showing a single process (without its threads).
 | 
						||
   *
 | 
						||
   * @param {ProcessDelta} data The data to display.
 | 
						||
   * @param {Number} maxSlopeCpu The largest slopeCpu value.
 | 
						||
   * @return {DOMElement} The row displaying the process.
 | 
						||
   */
 | 
						||
  displayProcessRow(data, maxSlopeCpu) {
 | 
						||
    const cellCount = 4;
 | 
						||
    let rowId = "p:" + data.pid;
 | 
						||
    let row = this._getOrCreateRow(rowId, cellCount);
 | 
						||
    row.process = data;
 | 
						||
    {
 | 
						||
      let classNames = "process";
 | 
						||
      if (data.isHung) {
 | 
						||
        classNames += " hung";
 | 
						||
      }
 | 
						||
      row.className = classNames;
 | 
						||
    }
 | 
						||
 | 
						||
    // Column: Name
 | 
						||
    let nameCell = row.firstChild;
 | 
						||
    {
 | 
						||
      let classNames = [];
 | 
						||
      let fluentName;
 | 
						||
      let fluentArgs = {
 | 
						||
        pid: "" + data.pid, // Make sure that this number is not localized
 | 
						||
      };
 | 
						||
      switch (data.type) {
 | 
						||
        case "web":
 | 
						||
          fluentName = "about-processes-web-process";
 | 
						||
          break;
 | 
						||
        case "webIsolated":
 | 
						||
          fluentName = "about-processes-web-isolated-process";
 | 
						||
          fluentArgs.origin = data.origin;
 | 
						||
          break;
 | 
						||
        case "webServiceWorker":
 | 
						||
          fluentName = "about-processes-web-serviceworker";
 | 
						||
          fluentArgs.origin = data.origin;
 | 
						||
          break;
 | 
						||
        case "file":
 | 
						||
          fluentName = "about-processes-file-process";
 | 
						||
          break;
 | 
						||
        case "extension":
 | 
						||
          fluentName = "about-processes-extension-process";
 | 
						||
          classNames = ["extensions"];
 | 
						||
          break;
 | 
						||
        case "privilegedabout":
 | 
						||
          fluentName = "about-processes-privilegedabout-process";
 | 
						||
          break;
 | 
						||
        case "privilegedmozilla":
 | 
						||
          fluentName = "about-processes-privilegedmozilla-process";
 | 
						||
          break;
 | 
						||
        case "withCoopCoep":
 | 
						||
          fluentName = "about-processes-with-coop-coep-process";
 | 
						||
          fluentArgs.origin = data.origin;
 | 
						||
          break;
 | 
						||
        case "browser":
 | 
						||
          fluentName = "about-processes-browser-process";
 | 
						||
          break;
 | 
						||
        case "plugin":
 | 
						||
          fluentName = "about-processes-plugin-process";
 | 
						||
          break;
 | 
						||
        case "gmpPlugin":
 | 
						||
          fluentName = "about-processes-gmp-plugin-process";
 | 
						||
          break;
 | 
						||
        case "gpu":
 | 
						||
          fluentName = "about-processes-gpu-process";
 | 
						||
          break;
 | 
						||
        case "vr":
 | 
						||
          fluentName = "about-processes-vr-process";
 | 
						||
          break;
 | 
						||
        case "rdd":
 | 
						||
          fluentName = "about-processes-rdd-process";
 | 
						||
          break;
 | 
						||
        case "socket":
 | 
						||
          fluentName = "about-processes-socket-process";
 | 
						||
          break;
 | 
						||
        case "remoteSandboxBroker":
 | 
						||
          fluentName = "about-processes-remote-sandbox-broker-process";
 | 
						||
          break;
 | 
						||
        case "forkServer":
 | 
						||
          fluentName = "about-processes-fork-server-process";
 | 
						||
          break;
 | 
						||
        case "preallocated":
 | 
						||
          fluentName = "about-processes-preallocated-process";
 | 
						||
          break;
 | 
						||
        case "utility":
 | 
						||
          fluentName = "about-processes-utility-process";
 | 
						||
          break;
 | 
						||
        // The following are probably not going to show up for users
 | 
						||
        // but let's handle the case anyway to avoid heisenoranges
 | 
						||
        // during tests in case of a leftover process from a previous
 | 
						||
        // test.
 | 
						||
        default:
 | 
						||
          fluentName = "about-processes-unknown-process";
 | 
						||
          fluentArgs.type = data.type;
 | 
						||
          break;
 | 
						||
      }
 | 
						||
 | 
						||
      // Show container names instead of raw origin attribute suffixes.
 | 
						||
      if (fluentArgs.origin?.includes("^")) {
 | 
						||
        let origin = fluentArgs.origin;
 | 
						||
        let privateBrowsingId, userContextId;
 | 
						||
        try {
 | 
						||
          ({ privateBrowsingId, userContextId } =
 | 
						||
            ChromeUtils.createOriginAttributesFromOrigin(origin));
 | 
						||
          fluentArgs.origin = origin.slice(0, origin.indexOf("^"));
 | 
						||
        } catch (e) {
 | 
						||
          // createOriginAttributesFromOrigin can throw NS_ERROR_FAILURE for incorrect origin strings.
 | 
						||
        }
 | 
						||
        if (userContextId) {
 | 
						||
          let identityLabel =
 | 
						||
            ContextualIdentityService.getUserContextLabel(userContextId);
 | 
						||
          if (identityLabel) {
 | 
						||
            fluentArgs.origin += ` — ${identityLabel}`;
 | 
						||
          }
 | 
						||
        }
 | 
						||
        if (privateBrowsingId) {
 | 
						||
          fluentName += "-private";
 | 
						||
        }
 | 
						||
      }
 | 
						||
 | 
						||
      let processNameElement = nameCell;
 | 
						||
      if (SHOW_PROFILER_ICONS) {
 | 
						||
        if (!nameCell.firstChild) {
 | 
						||
          processNameElement = document.createElement("span");
 | 
						||
          nameCell.appendChild(processNameElement);
 | 
						||
 | 
						||
          let profilerIcon = document.createElement("span");
 | 
						||
          profilerIcon.className = "profiler-icon";
 | 
						||
          document.l10n.setAttributes(
 | 
						||
            profilerIcon,
 | 
						||
            "about-processes-profile-process",
 | 
						||
            { duration: PROFILE_DURATION }
 | 
						||
          );
 | 
						||
          nameCell.appendChild(profilerIcon);
 | 
						||
        } else {
 | 
						||
          processNameElement = nameCell.firstChild;
 | 
						||
        }
 | 
						||
      }
 | 
						||
      document.l10n.setAttributes(processNameElement, fluentName, fluentArgs);
 | 
						||
      nameCell.className = ["type", "favicon", ...classNames].join(" ");
 | 
						||
      nameCell.setAttribute("id", data.pid + "-label");
 | 
						||
 | 
						||
      let image;
 | 
						||
      switch (data.type) {
 | 
						||
        case "browser":
 | 
						||
        case "privilegedabout":
 | 
						||
          image = "chrome://branding/content/icon32.png";
 | 
						||
          break;
 | 
						||
        case "extension":
 | 
						||
          image = "chrome://mozapps/skin/extensions/extension.svg";
 | 
						||
          break;
 | 
						||
        default:
 | 
						||
          // If all favicons match, pick the shared favicon.
 | 
						||
          // Otherwise, pick a default icon.
 | 
						||
          // If some tabs have no favicon, we ignore them.
 | 
						||
          for (let win of data.windows || []) {
 | 
						||
            if (!win.tab) {
 | 
						||
              continue;
 | 
						||
            }
 | 
						||
            let favicon = win.tab.tab.getAttribute("image");
 | 
						||
            if (!favicon) {
 | 
						||
              // No favicon here, let's ignore the tab.
 | 
						||
            } else if (!image) {
 | 
						||
              // Let's pick a first favicon.
 | 
						||
              // We'll remove it later if we find conflicting favicons.
 | 
						||
              image = favicon;
 | 
						||
            } else if (image == favicon) {
 | 
						||
              // So far, no conflict, keep the favicon.
 | 
						||
            } else {
 | 
						||
              // Conflicting favicons, fallback to default.
 | 
						||
              image = null;
 | 
						||
              break;
 | 
						||
            }
 | 
						||
          }
 | 
						||
          if (!image) {
 | 
						||
            image = "chrome://global/skin/icons/link.svg";
 | 
						||
          }
 | 
						||
      }
 | 
						||
      nameCell.style.backgroundImage = `url('${image}')`;
 | 
						||
    }
 | 
						||
 | 
						||
    // Column: Memory
 | 
						||
    let memoryCell = nameCell.nextSibling;
 | 
						||
    {
 | 
						||
      let formattedTotal = this._formatMemory(data.totalRamSize);
 | 
						||
      if (data.deltaRamSize) {
 | 
						||
        let formattedDelta = this._formatMemory(data.deltaRamSize);
 | 
						||
        this._fillCell(memoryCell, {
 | 
						||
          fluentName: "about-processes-total-memory-size-changed",
 | 
						||
          fluentArgs: {
 | 
						||
            total: formattedTotal.amount,
 | 
						||
            totalUnit: gLocalizedUnits.memory[formattedTotal.unit],
 | 
						||
            delta: Math.abs(formattedDelta.amount),
 | 
						||
            deltaUnit: gLocalizedUnits.memory[formattedDelta.unit],
 | 
						||
            deltaSign: data.deltaRamSize > 0 ? "+" : "-",
 | 
						||
          },
 | 
						||
          classes: ["memory"],
 | 
						||
        });
 | 
						||
      } else {
 | 
						||
        this._fillCell(memoryCell, {
 | 
						||
          fluentName: "about-processes-total-memory-size-no-change",
 | 
						||
          fluentArgs: {
 | 
						||
            total: formattedTotal.amount,
 | 
						||
            totalUnit: gLocalizedUnits.memory[formattedTotal.unit],
 | 
						||
          },
 | 
						||
          classes: ["memory"],
 | 
						||
        });
 | 
						||
      }
 | 
						||
    }
 | 
						||
 | 
						||
    // Column: CPU
 | 
						||
    let cpuCell = memoryCell.nextSibling;
 | 
						||
    this.displayCpu(data, cpuCell, maxSlopeCpu);
 | 
						||
 | 
						||
    // Column: Kill button – but not for all processes.
 | 
						||
    let killButton = cpuCell.nextSibling;
 | 
						||
    killButton.className = "action-icon";
 | 
						||
 | 
						||
    if (data.type.startsWith("web")) {
 | 
						||
      // This type of process can be killed.
 | 
						||
      if (this._killedRecently.some(kill => kill.pid && kill.pid == data.pid)) {
 | 
						||
        // We're racing between the "kill" action and the visual refresh.
 | 
						||
        // In a few cases, we could end up with the visual refresh showing
 | 
						||
        // a process as un-killed while we actually just killed it.
 | 
						||
        //
 | 
						||
        // We still want to display the process in case something actually
 | 
						||
        // went bad and the user needs the information to realize this.
 | 
						||
        // But we also want to make it visible that the process is being
 | 
						||
        // killed.
 | 
						||
        row.classList.add("killed");
 | 
						||
      } else {
 | 
						||
        // Otherwise, let's display the kill button.
 | 
						||
        killButton.classList.add("close-icon");
 | 
						||
        document.l10n.setAttributes(
 | 
						||
          killButton,
 | 
						||
          "about-processes-shutdown-process"
 | 
						||
        );
 | 
						||
      }
 | 
						||
    }
 | 
						||
 | 
						||
    return row;
 | 
						||
  },
 | 
						||
 | 
						||
  /**
 | 
						||
   * Display a thread summary row with the thread count and a twisty to
 | 
						||
   * open/close the list.
 | 
						||
   *
 | 
						||
   * @param {ProcessDelta} data The data to display.
 | 
						||
   * @return {boolean} Whether the full thread list should be displayed.
 | 
						||
   */
 | 
						||
  displayThreadSummaryRow(data) {
 | 
						||
    const cellCount = 2;
 | 
						||
    let rowId = "ts:" + data.pid;
 | 
						||
    let row = this._getOrCreateRow(rowId, cellCount);
 | 
						||
    row.process = data;
 | 
						||
    row.className = "thread-summary";
 | 
						||
    let isOpen = false;
 | 
						||
 | 
						||
    // Column: Name
 | 
						||
    let nameCell = row.firstChild;
 | 
						||
    let threads = data.threads;
 | 
						||
    let activeThreads = new Map();
 | 
						||
    let activeThreadCount = 0;
 | 
						||
    for (let t of data.threads) {
 | 
						||
      if (!t.active) {
 | 
						||
        continue;
 | 
						||
      }
 | 
						||
      ++activeThreadCount;
 | 
						||
      let name = t.name.replace(/ ?#[0-9]+$/, "");
 | 
						||
      if (!activeThreads.has(name)) {
 | 
						||
        activeThreads.set(name, { name, slopeCpu: t.slopeCpu, count: 1 });
 | 
						||
      } else {
 | 
						||
        let thread = activeThreads.get(name);
 | 
						||
        thread.count++;
 | 
						||
        thread.slopeCpu += t.slopeCpu;
 | 
						||
      }
 | 
						||
    }
 | 
						||
    let fluentName, fluentArgs;
 | 
						||
    if (activeThreadCount) {
 | 
						||
      let percentFormatter = new Intl.NumberFormat(undefined, {
 | 
						||
        style: "percent",
 | 
						||
        minimumSignificantDigits: 1,
 | 
						||
      });
 | 
						||
 | 
						||
      let threadList = Array.from(activeThreads.values());
 | 
						||
      threadList.sort((t1, t2) => t2.slopeCpu - t1.slopeCpu);
 | 
						||
 | 
						||
      fluentName = "about-processes-active-threads";
 | 
						||
      fluentArgs = {
 | 
						||
        number: threads.length,
 | 
						||
        active: activeThreadCount,
 | 
						||
        list: new Intl.ListFormat(undefined, { style: "narrow" }).format(
 | 
						||
          threadList.map(t => {
 | 
						||
            let name = t.count > 1 ? `${t.count} × ${t.name}` : t.name;
 | 
						||
            let percent = Math.round(t.slopeCpu * 1000) / 1000;
 | 
						||
            if (percent) {
 | 
						||
              return `${name} ${percentFormatter.format(percent)}`;
 | 
						||
            }
 | 
						||
            return name;
 | 
						||
          })
 | 
						||
        ),
 | 
						||
      };
 | 
						||
    } else {
 | 
						||
      fluentName = "about-processes-inactive-threads";
 | 
						||
      fluentArgs = {
 | 
						||
        number: threads.length,
 | 
						||
      };
 | 
						||
    }
 | 
						||
 | 
						||
    let span;
 | 
						||
    if (!nameCell.firstChild) {
 | 
						||
      nameCell.className = "name indent";
 | 
						||
      // Create the nodes:
 | 
						||
      let imgBtn = document.createElement("span");
 | 
						||
      // Provide markup for an accessible disclosure button:
 | 
						||
      imgBtn.className = "twisty";
 | 
						||
      imgBtn.setAttribute("role", "button");
 | 
						||
      imgBtn.setAttribute("tabindex", "0");
 | 
						||
      // Label to include both summary and details texts
 | 
						||
      imgBtn.setAttribute("aria-labelledby", `${data.pid}-label ${rowId}`);
 | 
						||
      if (!imgBtn.hasAttribute("aria-expanded")) {
 | 
						||
        imgBtn.setAttribute("aria-expanded", "false");
 | 
						||
      }
 | 
						||
      nameCell.appendChild(imgBtn);
 | 
						||
 | 
						||
      span = document.createElement("span");
 | 
						||
      span.setAttribute("id", rowId);
 | 
						||
      nameCell.appendChild(span);
 | 
						||
    } else {
 | 
						||
      // The only thing that can change is the thread count.
 | 
						||
      let imgBtn = nameCell.firstChild;
 | 
						||
      isOpen = imgBtn.classList.contains("open");
 | 
						||
      span = imgBtn.nextSibling;
 | 
						||
    }
 | 
						||
    document.l10n.setAttributes(span, fluentName, fluentArgs);
 | 
						||
 | 
						||
    // Column: action
 | 
						||
    let actionCell = nameCell.nextSibling;
 | 
						||
    actionCell.className = "action-icon";
 | 
						||
 | 
						||
    return isOpen;
 | 
						||
  },
 | 
						||
 | 
						||
  displayDOMWindowRow(data) {
 | 
						||
    const cellCount = 2;
 | 
						||
    let rowId = "w:" + data.outerWindowId;
 | 
						||
    let row = this._getOrCreateRow(rowId, cellCount);
 | 
						||
    row.win = data;
 | 
						||
    row.className = "window";
 | 
						||
 | 
						||
    // Column: name
 | 
						||
    let nameCell = row.firstChild;
 | 
						||
    let tab = tabFinder.get(data.outerWindowId);
 | 
						||
    let fluentName;
 | 
						||
    let fluentArgs = {};
 | 
						||
    let className;
 | 
						||
    if (tab && tab.tabbrowser) {
 | 
						||
      fluentName = "about-processes-tab-name";
 | 
						||
      fluentArgs.name = tab.tab.label;
 | 
						||
      className = "tab";
 | 
						||
    } else if (tab) {
 | 
						||
      fluentName = "about-processes-preloaded-tab";
 | 
						||
      className = "preloaded-tab";
 | 
						||
    } else if (data.count == 1) {
 | 
						||
      fluentName = "about-processes-frame-name-one";
 | 
						||
      fluentArgs.url = data.documentURI.spec;
 | 
						||
      className = "frame-one";
 | 
						||
    } else {
 | 
						||
      fluentName = "about-processes-frame-name-many";
 | 
						||
      fluentArgs.number = data.count;
 | 
						||
      fluentArgs.shortUrl =
 | 
						||
        data.documentURI.scheme == "about"
 | 
						||
          ? data.documentURI.spec
 | 
						||
          : data.documentURI.prePath;
 | 
						||
      className = "frame-many";
 | 
						||
    }
 | 
						||
    this._fillCell(nameCell, {
 | 
						||
      fluentName,
 | 
						||
      fluentArgs,
 | 
						||
      classes: ["name", "indent", "favicon", className],
 | 
						||
    });
 | 
						||
    let image = tab?.tab.getAttribute("image");
 | 
						||
    if (image) {
 | 
						||
      nameCell.style.backgroundImage = `url('${image}')`;
 | 
						||
    }
 | 
						||
 | 
						||
    // Column: action
 | 
						||
    let killButton = nameCell.nextSibling;
 | 
						||
    killButton.className = "action-icon";
 | 
						||
 | 
						||
    if (data.tab && data.tab.tabbrowser) {
 | 
						||
      // A tab. We want to be able to close it.
 | 
						||
      if (
 | 
						||
        this._killedRecently.some(
 | 
						||
          kill => kill.windowId && kill.windowId == data.outerWindowId
 | 
						||
        )
 | 
						||
      ) {
 | 
						||
        // We're racing between the "kill" action and the visual refresh.
 | 
						||
        // In a few cases, we could end up with the visual refresh showing
 | 
						||
        // a window as un-killed while we actually just killed it.
 | 
						||
        //
 | 
						||
        // We still want to display the window in case something actually
 | 
						||
        // went bad and the user needs the information to realize this.
 | 
						||
        // But we also want to make it visible that the window is being
 | 
						||
        // killed.
 | 
						||
        row.classList.add("killed");
 | 
						||
      } else {
 | 
						||
        // Otherwise, let's display the kill button.
 | 
						||
        killButton.classList.add("close-icon");
 | 
						||
        document.l10n.setAttributes(killButton, "about-processes-shutdown-tab");
 | 
						||
      }
 | 
						||
    }
 | 
						||
  },
 | 
						||
 | 
						||
  utilityActorNameToFluentName(actorName) {
 | 
						||
    let fluentName;
 | 
						||
    switch (actorName) {
 | 
						||
      case "audioDecoder_Generic":
 | 
						||
        fluentName = "about-processes-utility-actor-audio-decoder-generic";
 | 
						||
        break;
 | 
						||
 | 
						||
      case "audioDecoder_AppleMedia":
 | 
						||
        fluentName = "about-processes-utility-actor-audio-decoder-applemedia";
 | 
						||
        break;
 | 
						||
 | 
						||
      case "audioDecoder_WMF":
 | 
						||
        fluentName = "about-processes-utility-actor-audio-decoder-wmf";
 | 
						||
        break;
 | 
						||
 | 
						||
      case "mfMediaEngineCDM":
 | 
						||
        fluentName = "about-processes-utility-actor-mf-media-engine";
 | 
						||
        break;
 | 
						||
 | 
						||
      case "jSOracle":
 | 
						||
        fluentName = "about-processes-utility-actor-js-oracle";
 | 
						||
        break;
 | 
						||
 | 
						||
      case "windowsUtils":
 | 
						||
        fluentName = "about-processes-utility-actor-windows-utils";
 | 
						||
        break;
 | 
						||
 | 
						||
      case "windowsFileDialog":
 | 
						||
        fluentName = "about-processes-utility-actor-windows-file-dialog";
 | 
						||
        break;
 | 
						||
 | 
						||
      default:
 | 
						||
        fluentName = "about-processes-utility-actor-unknown";
 | 
						||
        break;
 | 
						||
    }
 | 
						||
    return fluentName;
 | 
						||
  },
 | 
						||
 | 
						||
  displayUtilityActorRow(data, parent) {
 | 
						||
    const cellCount = 2;
 | 
						||
    // The actor name is expected to be unique within a given utility process.
 | 
						||
    let rowId = "u:" + parent.pid + data.actorName;
 | 
						||
    let row = this._getOrCreateRow(rowId, cellCount);
 | 
						||
    row.actor = data;
 | 
						||
    row.className = "actor";
 | 
						||
 | 
						||
    // Column: name
 | 
						||
    let nameCell = row.firstChild;
 | 
						||
    let fluentName = this.utilityActorNameToFluentName(data.actorName);
 | 
						||
    let fluentArgs = {};
 | 
						||
    this._fillCell(nameCell, {
 | 
						||
      fluentName,
 | 
						||
      fluentArgs,
 | 
						||
      classes: ["name", "indent", "favicon"],
 | 
						||
    });
 | 
						||
  },
 | 
						||
 | 
						||
  /**
 | 
						||
   * Display a row showing a single thread.
 | 
						||
   *
 | 
						||
   * @param {ThreadDelta} data The data to display.
 | 
						||
   * @param {Number} maxSlopeCpu The largest slopeCpu value.
 | 
						||
   */
 | 
						||
  displayThreadRow(data, maxSlopeCpu) {
 | 
						||
    const cellCount = 3;
 | 
						||
    let rowId = "t:" + data.tid;
 | 
						||
    let row = this._getOrCreateRow(rowId, cellCount);
 | 
						||
    row.thread = data;
 | 
						||
    row.className = "thread";
 | 
						||
 | 
						||
    // Column: name
 | 
						||
    let nameCell = row.firstChild;
 | 
						||
    this._fillCell(nameCell, {
 | 
						||
      fluentName: "about-processes-thread-name-and-id",
 | 
						||
      fluentArgs: {
 | 
						||
        name: data.name,
 | 
						||
        tid: "" + data.tid /* Make sure that this number is not localized */,
 | 
						||
      },
 | 
						||
      classes: ["name", "double_indent"],
 | 
						||
    });
 | 
						||
 | 
						||
    // Column: CPU
 | 
						||
    this.displayCpu(data, nameCell.nextSibling, maxSlopeCpu);
 | 
						||
 | 
						||
    // Third column (Buttons) is empty, nothing to do.
 | 
						||
  },
 | 
						||
 | 
						||
  _orderedRows: [],
 | 
						||
  _fillCell(elt, { classes, fluentName, fluentArgs }) {
 | 
						||
    document.l10n.setAttributes(elt, fluentName, fluentArgs);
 | 
						||
    elt.className = classes.join(" ");
 | 
						||
  },
 | 
						||
 | 
						||
  _getDuration(rawDurationNS) {
 | 
						||
    if (rawDurationNS <= NS_PER_US) {
 | 
						||
      return { duration: rawDurationNS, unit: "ns" };
 | 
						||
    }
 | 
						||
    if (rawDurationNS <= NS_PER_MS) {
 | 
						||
      return { duration: rawDurationNS / NS_PER_US, unit: "us" };
 | 
						||
    }
 | 
						||
    if (rawDurationNS <= NS_PER_S) {
 | 
						||
      return { duration: rawDurationNS / NS_PER_MS, unit: "ms" };
 | 
						||
    }
 | 
						||
    if (rawDurationNS <= NS_PER_MIN) {
 | 
						||
      return { duration: rawDurationNS / NS_PER_S, unit: "s" };
 | 
						||
    }
 | 
						||
    if (rawDurationNS <= NS_PER_HOUR) {
 | 
						||
      return { duration: rawDurationNS / NS_PER_MIN, unit: "m" };
 | 
						||
    }
 | 
						||
    if (rawDurationNS <= NS_PER_DAY) {
 | 
						||
      return { duration: rawDurationNS / NS_PER_HOUR, unit: "h" };
 | 
						||
    }
 | 
						||
    return { duration: rawDurationNS / NS_PER_DAY, unit: "d" };
 | 
						||
  },
 | 
						||
 | 
						||
  /**
 | 
						||
   * Format a value representing an amount of memory.
 | 
						||
   *
 | 
						||
   * As a special case, we also handle `null`, which represents the case in which we do
 | 
						||
   * not have sufficient information to compute an amount of memory.
 | 
						||
   *
 | 
						||
   * @param {Number?} value The value to format. Must be either `null` or a non-negative number.
 | 
						||
   * @return { {unit: "GB" | "MB" | "KB" | B" | "?"}, amount: Number } The formated amount and its
 | 
						||
   *  unit, which may be used for e.g. additional CSS formating.
 | 
						||
   */
 | 
						||
  _formatMemory(value) {
 | 
						||
    if (value == null) {
 | 
						||
      return { unit: "?", amount: 0 };
 | 
						||
    }
 | 
						||
    if (typeof value != "number") {
 | 
						||
      throw new Error(`Invalid memory value ${value}`);
 | 
						||
    }
 | 
						||
    let abs = Math.abs(value);
 | 
						||
    if (abs >= ONE_GIGA) {
 | 
						||
      return {
 | 
						||
        unit: "GB",
 | 
						||
        amount: value / ONE_GIGA,
 | 
						||
      };
 | 
						||
    }
 | 
						||
    if (abs >= ONE_MEGA) {
 | 
						||
      return {
 | 
						||
        unit: "MB",
 | 
						||
        amount: value / ONE_MEGA,
 | 
						||
      };
 | 
						||
    }
 | 
						||
    if (abs >= ONE_KILO) {
 | 
						||
      return {
 | 
						||
        unit: "KB",
 | 
						||
        amount: value / ONE_KILO,
 | 
						||
      };
 | 
						||
    }
 | 
						||
    return {
 | 
						||
      unit: "B",
 | 
						||
      amount: value,
 | 
						||
    };
 | 
						||
  },
 | 
						||
};
 | 
						||
 | 
						||
var Control = {
 | 
						||
  // The set of all processes reported as "hung" by the process hang monitor.
 | 
						||
  //
 | 
						||
  // type: Set<ChildID>
 | 
						||
  _hungItems: new Set(),
 | 
						||
  _sortColumn: null,
 | 
						||
  _sortAscendent: true,
 | 
						||
  _removeSubtree(row) {
 | 
						||
    let sibling = row.nextSibling;
 | 
						||
    while (sibling && !sibling.classList.contains("process")) {
 | 
						||
      let next = sibling.nextSibling;
 | 
						||
      if (sibling.classList.contains("thread")) {
 | 
						||
        View._removeRow(sibling);
 | 
						||
      }
 | 
						||
      sibling = next;
 | 
						||
    }
 | 
						||
  },
 | 
						||
  init() {
 | 
						||
    this._initHangReports();
 | 
						||
 | 
						||
    // Start prefetching units.
 | 
						||
    this._promisePrefetchedUnits = (async function () {
 | 
						||
      let [ns, us, ms, s, m, h, d, B, KB, MB, GB, TB, PB, EB] =
 | 
						||
        await document.l10n.formatValues([
 | 
						||
          { id: "duration-unit-ns" },
 | 
						||
          { id: "duration-unit-us" },
 | 
						||
          { id: "duration-unit-ms" },
 | 
						||
          { id: "duration-unit-s" },
 | 
						||
          { id: "duration-unit-m" },
 | 
						||
          { id: "duration-unit-h" },
 | 
						||
          { id: "duration-unit-d" },
 | 
						||
          { id: "memory-unit-B" },
 | 
						||
          { id: "memory-unit-KB" },
 | 
						||
          { id: "memory-unit-MB" },
 | 
						||
          { id: "memory-unit-GB" },
 | 
						||
          { id: "memory-unit-TB" },
 | 
						||
          { id: "memory-unit-PB" },
 | 
						||
          { id: "memory-unit-EB" },
 | 
						||
        ]);
 | 
						||
      return {
 | 
						||
        duration: { ns, us, ms, s, m, h, d },
 | 
						||
        memory: { B, KB, MB, GB, TB, PB, EB },
 | 
						||
      };
 | 
						||
    })();
 | 
						||
 | 
						||
    let tbody = document.getElementById("process-tbody");
 | 
						||
 | 
						||
    // Single click:
 | 
						||
    // - show or hide the contents of a twisty;
 | 
						||
    // - close a process;
 | 
						||
    // - profile a process;
 | 
						||
    // - change selection.
 | 
						||
    tbody.addEventListener("click", event => {
 | 
						||
      this._updateLastMouseEvent();
 | 
						||
 | 
						||
      this._handleActivate(event.target);
 | 
						||
    });
 | 
						||
 | 
						||
    // Enter or Space keypress:
 | 
						||
    // - show or hide the contents of a twisty;
 | 
						||
    // - close a process;
 | 
						||
    // - profile a process;
 | 
						||
    // - change selection.
 | 
						||
    tbody.addEventListener("keypress", event => {
 | 
						||
      // Handle showing or hiding subitems of a row, when keyboard is used.
 | 
						||
      if (event.key === "Enter" || event.key === " ") {
 | 
						||
        this._handleActivate(event.target);
 | 
						||
      }
 | 
						||
    });
 | 
						||
 | 
						||
    // Double click:
 | 
						||
    // - navigate to tab;
 | 
						||
    // - navigate to about:addons.
 | 
						||
    tbody.addEventListener("dblclick", event => {
 | 
						||
      this._updateLastMouseEvent();
 | 
						||
      event.stopPropagation();
 | 
						||
 | 
						||
      // Bubble up the doubleclick manually.
 | 
						||
      for (
 | 
						||
        let target = event.target;
 | 
						||
        target && target.getAttribute("id") != "process-tbody";
 | 
						||
        target = target.parentNode
 | 
						||
      ) {
 | 
						||
        if (target.classList.contains("tab")) {
 | 
						||
          // We've clicked on a tab, navigate.
 | 
						||
          let { tab, tabbrowser } = target.parentNode.win.tab;
 | 
						||
          tabbrowser.selectedTab = tab;
 | 
						||
          tabbrowser.ownerGlobal.focus();
 | 
						||
          return;
 | 
						||
        }
 | 
						||
        if (target.classList.contains("extensions")) {
 | 
						||
          // We've clicked on the extensions process, open or reuse window.
 | 
						||
          let parentWin =
 | 
						||
            window.docShell.browsingContext.embedderElement.ownerGlobal;
 | 
						||
          parentWin.BrowserOpenAddonsMgr();
 | 
						||
          return;
 | 
						||
        }
 | 
						||
        // Otherwise, proceed.
 | 
						||
      }
 | 
						||
    });
 | 
						||
 | 
						||
    tbody.addEventListener("mousemove", () => {
 | 
						||
      this._updateLastMouseEvent();
 | 
						||
    });
 | 
						||
 | 
						||
    // Visibility change:
 | 
						||
    // - stop updating while the user isn't looking;
 | 
						||
    // - resume updating when the user returns.
 | 
						||
    window.addEventListener("visibilitychange", () => {
 | 
						||
      if (!document.hidden) {
 | 
						||
        this._updateDisplay(true);
 | 
						||
      }
 | 
						||
    });
 | 
						||
 | 
						||
    document
 | 
						||
      .getElementById("process-thead")
 | 
						||
      .addEventListener("click", async event => {
 | 
						||
        if (!event.target.classList.contains("clickable")) {
 | 
						||
          return;
 | 
						||
        }
 | 
						||
        // Linux has conventions opposite to Windows and macOS on the direction of arrows
 | 
						||
        // when sorting.
 | 
						||
        const platformIsLinux = AppConstants.platform == "linux";
 | 
						||
        const ascArrow = platformIsLinux ? "arrow-up" : "arrow-down";
 | 
						||
        const descArrow = platformIsLinux ? "arrow-down" : "arrow-up";
 | 
						||
 | 
						||
        if (this._sortColumn) {
 | 
						||
          const td = document.getElementById(this._sortColumn);
 | 
						||
          td.classList.remove(ascArrow, descArrow);
 | 
						||
        }
 | 
						||
 | 
						||
        const columnId = event.target.id;
 | 
						||
        if (columnId == this._sortColumn) {
 | 
						||
          // Reverse sorting order.
 | 
						||
          this._sortAscendent = !this._sortAscendent;
 | 
						||
        } else {
 | 
						||
          this._sortColumn = columnId;
 | 
						||
          this._sortAscendent = true;
 | 
						||
        }
 | 
						||
 | 
						||
        event.target.classList.toggle(ascArrow, this._sortAscendent);
 | 
						||
        event.target.classList.toggle(descArrow, !this._sortAscendent);
 | 
						||
 | 
						||
        await this._updateDisplay(true);
 | 
						||
      });
 | 
						||
  },
 | 
						||
  _lastMouseEvent: 0,
 | 
						||
  _updateLastMouseEvent() {
 | 
						||
    this._lastMouseEvent = Date.now();
 | 
						||
  },
 | 
						||
  _initHangReports() {
 | 
						||
    const PROCESS_HANG_REPORT_NOTIFICATION = "process-hang-report";
 | 
						||
 | 
						||
    // Receiving report of a hung child.
 | 
						||
    // Let's store if for our next update.
 | 
						||
    let hangReporter = report => {
 | 
						||
      report.QueryInterface(Ci.nsIHangReport);
 | 
						||
      this._hungItems.add(report.childID);
 | 
						||
    };
 | 
						||
    Services.obs.addObserver(hangReporter, PROCESS_HANG_REPORT_NOTIFICATION);
 | 
						||
 | 
						||
    // Don't forget to unregister the reporter.
 | 
						||
    window.addEventListener(
 | 
						||
      "unload",
 | 
						||
      () => {
 | 
						||
        Services.obs.removeObserver(
 | 
						||
          hangReporter,
 | 
						||
          PROCESS_HANG_REPORT_NOTIFICATION
 | 
						||
        );
 | 
						||
      },
 | 
						||
      { once: true }
 | 
						||
    );
 | 
						||
  },
 | 
						||
  async update(force = false) {
 | 
						||
    await State.update(force);
 | 
						||
 | 
						||
    if (document.hidden) {
 | 
						||
      return;
 | 
						||
    }
 | 
						||
 | 
						||
    await this._updateDisplay(force);
 | 
						||
  },
 | 
						||
 | 
						||
  // The force parameter can force a full update even when the mouse has been
 | 
						||
  // moved recently.
 | 
						||
  async _updateDisplay(force = false) {
 | 
						||
    let counters = State.getCounters();
 | 
						||
    if (this._promisePrefetchedUnits) {
 | 
						||
      gLocalizedUnits = await this._promisePrefetchedUnits;
 | 
						||
      this._promisePrefetchedUnits = null;
 | 
						||
    }
 | 
						||
 | 
						||
    // We reset `_hungItems`, based on the assumption that the process hang
 | 
						||
    // monitor will inform us again before the next update. Since the process hang monitor
 | 
						||
    // pings its clients about once per second and we update about once per 2 seconds
 | 
						||
    // (or more if the mouse moves), we should be ok.
 | 
						||
    let hungItems = this._hungItems;
 | 
						||
    this._hungItems = new Set();
 | 
						||
 | 
						||
    counters = this._sortProcesses(counters);
 | 
						||
 | 
						||
    // Stored because it is used when opening the list of threads.
 | 
						||
    this._maxSlopeCpu = Math.max(...counters.map(process => process.slopeCpu));
 | 
						||
 | 
						||
    let previousProcess = null;
 | 
						||
    for (let process of counters) {
 | 
						||
      this._sortDOMWindows(process.windows);
 | 
						||
 | 
						||
      process.isHung = process.childID && hungItems.has(process.childID);
 | 
						||
 | 
						||
      let processRow = View.displayProcessRow(process, this._maxSlopeCpu);
 | 
						||
 | 
						||
      if (process.type != "extension") {
 | 
						||
        // We do not want to display extensions.
 | 
						||
        for (let win of process.windows) {
 | 
						||
          if (SHOW_ALL_SUBFRAMES || win.tab || win.isProcessRoot) {
 | 
						||
            View.displayDOMWindowRow(win, process);
 | 
						||
          }
 | 
						||
        }
 | 
						||
      }
 | 
						||
 | 
						||
      if (process.type === "utility") {
 | 
						||
        for (let actor of process.utilityActors) {
 | 
						||
          View.displayUtilityActorRow(actor, process);
 | 
						||
        }
 | 
						||
      }
 | 
						||
 | 
						||
      if (SHOW_THREADS) {
 | 
						||
        if (View.displayThreadSummaryRow(process)) {
 | 
						||
          this._showThreads(processRow, this._maxSlopeCpu);
 | 
						||
        }
 | 
						||
      }
 | 
						||
      if (
 | 
						||
        this._sortColumn == null &&
 | 
						||
        previousProcess &&
 | 
						||
        previousProcess.displayRank != process.displayRank
 | 
						||
      ) {
 | 
						||
        // Add a separation between successive categories of processes.
 | 
						||
        processRow.classList.add("separate-from-previous-process-group");
 | 
						||
      }
 | 
						||
      previousProcess = process;
 | 
						||
    }
 | 
						||
 | 
						||
    if (
 | 
						||
      !force &&
 | 
						||
      Date.now() - this._lastMouseEvent < TIME_BEFORE_SORTING_AGAIN
 | 
						||
    ) {
 | 
						||
      // If there has been a recent mouse event, we don't want to reorder,
 | 
						||
      // add or remove rows so that the table content under the mouse pointer
 | 
						||
      // doesn't change when the user might be about to click to close a tab
 | 
						||
      // or kill a process.
 | 
						||
      // We didn't return earlier because updating CPU and memory values is
 | 
						||
      // still valuable.
 | 
						||
      View.discardUpdate();
 | 
						||
      return;
 | 
						||
    }
 | 
						||
 | 
						||
    View.commit();
 | 
						||
 | 
						||
    // Reset the selectedRow field if that row is no longer in the DOM
 | 
						||
    // to avoid keeping forever references to dead processes.
 | 
						||
    if (this.selectedRow && !this.selectedRow.parentNode) {
 | 
						||
      this.selectedRow = null;
 | 
						||
    }
 | 
						||
 | 
						||
    // Used by tests to differentiate full updates from l10n updates.
 | 
						||
    document.dispatchEvent(new CustomEvent("AboutProcessesUpdated"));
 | 
						||
  },
 | 
						||
  _compareCpu(a, b) {
 | 
						||
    return (
 | 
						||
      b.slopeCpu - a.slopeCpu || b.active - a.active || b.totalCpu - a.totalCpu
 | 
						||
    );
 | 
						||
  },
 | 
						||
  _showThreads(row, maxSlopeCpu) {
 | 
						||
    let process = row.process;
 | 
						||
    this._sortThreads(process.threads);
 | 
						||
    for (let thread of process.threads) {
 | 
						||
      View.displayThreadRow(thread, maxSlopeCpu);
 | 
						||
    }
 | 
						||
  },
 | 
						||
  _sortThreads(threads) {
 | 
						||
    return threads.sort((a, b) => {
 | 
						||
      let order;
 | 
						||
      switch (this._sortColumn) {
 | 
						||
        case "column-name":
 | 
						||
          order = a.name.localeCompare(b.name) || a.tid - b.tid;
 | 
						||
          break;
 | 
						||
        case "column-cpu-total":
 | 
						||
          order = this._compareCpu(a, b);
 | 
						||
          break;
 | 
						||
        case "column-memory-resident":
 | 
						||
        case null:
 | 
						||
          order = a.tid - b.tid;
 | 
						||
          break;
 | 
						||
        default:
 | 
						||
          throw new Error("Unsupported order: " + this._sortColumn);
 | 
						||
      }
 | 
						||
      if (!this._sortAscendent) {
 | 
						||
        order = -order;
 | 
						||
      }
 | 
						||
      return order;
 | 
						||
    });
 | 
						||
  },
 | 
						||
  _sortProcesses(counters) {
 | 
						||
    return counters.sort((a, b) => {
 | 
						||
      let order;
 | 
						||
      switch (this._sortColumn) {
 | 
						||
        case "column-name":
 | 
						||
          order =
 | 
						||
            String(a.origin).localeCompare(b.origin) ||
 | 
						||
            String(a.type).localeCompare(b.type) ||
 | 
						||
            a.pid - b.pid;
 | 
						||
          break;
 | 
						||
        case "column-cpu-total":
 | 
						||
          order = this._compareCpu(a, b);
 | 
						||
          break;
 | 
						||
        case "column-memory-resident":
 | 
						||
          order = b.totalRamSize - a.totalRamSize;
 | 
						||
          break;
 | 
						||
        case null:
 | 
						||
          // Default order: classify processes by group.
 | 
						||
          order =
 | 
						||
            a.displayRank - b.displayRank ||
 | 
						||
            // Other processes are ordered by origin.
 | 
						||
            String(a.origin).localeCompare(b.origin);
 | 
						||
          break;
 | 
						||
        default:
 | 
						||
          throw new Error("Unsupported order: " + this._sortColumn);
 | 
						||
      }
 | 
						||
      if (!this._sortAscendent) {
 | 
						||
        order = -order;
 | 
						||
      }
 | 
						||
      return order;
 | 
						||
    });
 | 
						||
  },
 | 
						||
  _sortDOMWindows(windows) {
 | 
						||
    return windows.sort((a, b) => {
 | 
						||
      let order =
 | 
						||
        a.displayRank - b.displayRank ||
 | 
						||
        a.documentTitle.localeCompare(b.documentTitle) ||
 | 
						||
        a.documentURI.spec.localeCompare(b.documentURI.spec);
 | 
						||
      if (!this._sortAscendent) {
 | 
						||
        order = -order;
 | 
						||
      }
 | 
						||
      return order;
 | 
						||
    });
 | 
						||
  },
 | 
						||
 | 
						||
  // Assign a display rank to a process.
 | 
						||
  //
 | 
						||
  // The `browser` process comes first (rank 0).
 | 
						||
  // Then come web tabs (rank 1).
 | 
						||
  // Then come web frames (rank 2).
 | 
						||
  // Then come special processes (minus preallocated) (rank 3).
 | 
						||
  // Then come preallocated processes (rank 4).
 | 
						||
  _getDisplayGroupRank(data, windows) {
 | 
						||
    const RANK_BROWSER = 0;
 | 
						||
    const RANK_WEB_TABS = 1;
 | 
						||
    const RANK_WEB_FRAMES = 2;
 | 
						||
    const RANK_UTILITY = 3;
 | 
						||
    const RANK_PREALLOCATED = 4;
 | 
						||
    let type = data.type;
 | 
						||
    switch (type) {
 | 
						||
      // Browser comes first.
 | 
						||
      case "browser":
 | 
						||
        return RANK_BROWSER;
 | 
						||
      // Web content comes next.
 | 
						||
      case "webIsolated":
 | 
						||
      case "webServiceWorker":
 | 
						||
      case "withCoopCoep": {
 | 
						||
        if (windows.some(w => w.tab)) {
 | 
						||
          return RANK_WEB_TABS;
 | 
						||
        }
 | 
						||
        return RANK_WEB_FRAMES;
 | 
						||
      }
 | 
						||
      // Preallocated processes come last.
 | 
						||
      case "preallocated":
 | 
						||
        return RANK_PREALLOCATED;
 | 
						||
      // "web" is special, as it could be one of:
 | 
						||
      // - web content currently loading/unloading/...
 | 
						||
      // - a preallocated process.
 | 
						||
      case "web":
 | 
						||
        if (windows.some(w => w.tab)) {
 | 
						||
          return RANK_WEB_TABS;
 | 
						||
        }
 | 
						||
        if (windows.length >= 1) {
 | 
						||
          return RANK_WEB_FRAMES;
 | 
						||
        }
 | 
						||
        // For the time being, we do not display DOM workers
 | 
						||
        // (and there's no API to get information on them).
 | 
						||
        // Once the blockers for bug 1663737 have landed, we'll be able
 | 
						||
        // to find out whether this process has DOM workers. If so, we'll
 | 
						||
        // count this process as a content process.
 | 
						||
        return RANK_PREALLOCATED;
 | 
						||
      // Other special processes before preallocated.
 | 
						||
      default:
 | 
						||
        return RANK_UTILITY;
 | 
						||
    }
 | 
						||
  },
 | 
						||
 | 
						||
  // Handle events on image controls.
 | 
						||
  _handleActivate(target) {
 | 
						||
    if (target.classList.contains("twisty")) {
 | 
						||
      this._handleTwisty(target);
 | 
						||
      return;
 | 
						||
    }
 | 
						||
    if (target.classList.contains("close-icon")) {
 | 
						||
      this._handleKill(target);
 | 
						||
      return;
 | 
						||
    }
 | 
						||
 | 
						||
    if (target.classList.contains("profiler-icon")) {
 | 
						||
      this._handleProfiling(target);
 | 
						||
      return;
 | 
						||
    }
 | 
						||
 | 
						||
    this._handleSelection(target);
 | 
						||
  },
 | 
						||
 | 
						||
  // Open/close list of threads.
 | 
						||
  _handleTwisty(target) {
 | 
						||
    let row = target.parentNode.parentNode;
 | 
						||
    if (target.classList.toggle("open")) {
 | 
						||
      target.setAttribute("aria-expanded", "true");
 | 
						||
      this._showThreads(row, this._maxSlopeCpu);
 | 
						||
      View.insertAfterRow(row);
 | 
						||
    } else {
 | 
						||
      target.setAttribute("aria-expanded", "false");
 | 
						||
      this._removeSubtree(row);
 | 
						||
    }
 | 
						||
  },
 | 
						||
 | 
						||
  // Kill process/close tab/close subframe.
 | 
						||
  _handleKill(target) {
 | 
						||
    let row = target.parentNode;
 | 
						||
    if (row.process) {
 | 
						||
      // Kill process immediately.
 | 
						||
      let pid = row.process.pid;
 | 
						||
 | 
						||
      // Make sure that the user can't click twice on the kill button.
 | 
						||
      // Otherwise, chaos might ensue. Plus we risk crashing under Windows.
 | 
						||
      View._killedRecently.push({ pid });
 | 
						||
 | 
						||
      // Discard tab contents and show that the process and all its contents are getting killed.
 | 
						||
      row.classList.add("killing");
 | 
						||
      for (
 | 
						||
        let childRow = row.nextSibling;
 | 
						||
        childRow && !childRow.classList.contains("process");
 | 
						||
        childRow = childRow.nextSibling
 | 
						||
      ) {
 | 
						||
        childRow.classList.add("killing");
 | 
						||
        let win = childRow.win;
 | 
						||
        if (win) {
 | 
						||
          View._killedRecently.push({ pid: win.outerWindowId });
 | 
						||
          if (win.tab && win.tab.tabbrowser) {
 | 
						||
            win.tab.tabbrowser.discardBrowser(
 | 
						||
              win.tab.tab,
 | 
						||
              /* aForceDiscard = */ true
 | 
						||
            );
 | 
						||
          }
 | 
						||
        }
 | 
						||
      }
 | 
						||
 | 
						||
      // Finally, kill the process.
 | 
						||
      const ProcessTools = Cc["@mozilla.org/processtools-service;1"].getService(
 | 
						||
        Ci.nsIProcessToolsService
 | 
						||
      );
 | 
						||
      ProcessTools.kill(pid);
 | 
						||
    } else if (row.win && row.win.tab && row.win.tab.tabbrowser) {
 | 
						||
      // This is a tab, close it.
 | 
						||
      row.win.tab.tabbrowser.removeTab(row.win.tab.tab, {
 | 
						||
        skipPermitUnload: true,
 | 
						||
        animate: true,
 | 
						||
      });
 | 
						||
      View._killedRecently.push({ outerWindowId: row.win.outerWindowId });
 | 
						||
      row.classList.add("killing");
 | 
						||
 | 
						||
      // If this was the only root window of the process, show that the process is also getting killed.
 | 
						||
      if (row.previousSibling.classList.contains("process")) {
 | 
						||
        let parentRow = row.previousSibling;
 | 
						||
        let roots = 0;
 | 
						||
        for (let win of parentRow.process.windows) {
 | 
						||
          if (win.isProcessRoot) {
 | 
						||
            roots += 1;
 | 
						||
          }
 | 
						||
        }
 | 
						||
        if (roots <= 1) {
 | 
						||
          // Yes, we're the only process root, so the process is dying.
 | 
						||
          //
 | 
						||
          // It might actually become a preloaded process rather than
 | 
						||
          // dying. That's an acceptable error. Even if we display incorrectly
 | 
						||
          // that the process is dying, this error will last only one refresh.
 | 
						||
          View._killedRecently.push({ pid: parentRow.process.pid });
 | 
						||
          parentRow.classList.add("killing");
 | 
						||
        }
 | 
						||
      }
 | 
						||
    }
 | 
						||
  },
 | 
						||
 | 
						||
  // Handle profiling of a process.
 | 
						||
  _handleProfiling(target) {
 | 
						||
    if (Services.profiler.IsActive()) {
 | 
						||
      return;
 | 
						||
    }
 | 
						||
    Services.profiler.StartProfiler(
 | 
						||
      10000000,
 | 
						||
      1,
 | 
						||
      ["default", "ipcmessages", "power"],
 | 
						||
      ["pid:" + target.parentNode.parentNode.process.pid]
 | 
						||
    );
 | 
						||
    target.classList.add("profiler-active");
 | 
						||
    setTimeout(() => {
 | 
						||
      ProfilerPopupBackground.captureProfile("aboutprofiling");
 | 
						||
      target.classList.remove("profiler-active");
 | 
						||
    }, PROFILE_DURATION * 1000);
 | 
						||
  },
 | 
						||
 | 
						||
  // Handle selection changes.
 | 
						||
  _handleSelection(target) {
 | 
						||
    let row = target.closest("tr");
 | 
						||
    if (!row) {
 | 
						||
      return;
 | 
						||
    }
 | 
						||
    if (this.selectedRow) {
 | 
						||
      this.selectedRow.removeAttribute("selected");
 | 
						||
      if (this.selectedRow.rowId == row.rowId) {
 | 
						||
        // Clicking the same row again clears the selection.
 | 
						||
        this.selectedRow = null;
 | 
						||
        return;
 | 
						||
      }
 | 
						||
    }
 | 
						||
    row.setAttribute("selected", "true");
 | 
						||
    this.selectedRow = row;
 | 
						||
  },
 | 
						||
};
 | 
						||
 | 
						||
window.onload = async function () {
 | 
						||
  Control.init();
 | 
						||
 | 
						||
  // Display immediately the list of processes. CPU values will be missing.
 | 
						||
  await Control.update();
 | 
						||
 | 
						||
  // After the minimum interval between samples, force an update to show
 | 
						||
  // valid CPU values asap.
 | 
						||
  await new Promise(resolve =>
 | 
						||
    setTimeout(resolve, MINIMUM_INTERVAL_BETWEEN_SAMPLES_MS)
 | 
						||
  );
 | 
						||
  await Control.update(true);
 | 
						||
 | 
						||
  // Then update at the normal frequency.
 | 
						||
  window.setInterval(() => Control.update(), UPDATE_INTERVAL_MS);
 | 
						||
};
 |