mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 10:18:41 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			2624 lines
		
	
	
	
		
			76 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			2624 lines
		
	
	
	
		
			76 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* 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";
 | 
						|
 | 
						|
const { BrowserUtils } = ChromeUtils.importESModule(
 | 
						|
  "resource://gre/modules/BrowserUtils.sys.mjs"
 | 
						|
);
 | 
						|
const { TelemetryTimestamps } = ChromeUtils.importESModule(
 | 
						|
  "resource://gre/modules/TelemetryTimestamps.sys.mjs"
 | 
						|
);
 | 
						|
const { TelemetryController } = ChromeUtils.importESModule(
 | 
						|
  "resource://gre/modules/TelemetryController.sys.mjs"
 | 
						|
);
 | 
						|
const { TelemetryArchive } = ChromeUtils.importESModule(
 | 
						|
  "resource://gre/modules/TelemetryArchive.sys.mjs"
 | 
						|
);
 | 
						|
const { TelemetrySend } = ChromeUtils.importESModule(
 | 
						|
  "resource://gre/modules/TelemetrySend.sys.mjs"
 | 
						|
);
 | 
						|
 | 
						|
const { AppConstants } = ChromeUtils.importESModule(
 | 
						|
  "resource://gre/modules/AppConstants.sys.mjs"
 | 
						|
);
 | 
						|
ChromeUtils.defineESModuleGetters(this, {
 | 
						|
  ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs",
 | 
						|
  Preferences: "resource://gre/modules/Preferences.sys.mjs",
 | 
						|
});
 | 
						|
 | 
						|
const Telemetry = Services.telemetry;
 | 
						|
 | 
						|
// Maximum height of a histogram bar (in em for html, in chars for text)
 | 
						|
const MAX_BAR_HEIGHT = 8;
 | 
						|
const MAX_BAR_CHARS = 25;
 | 
						|
const PREF_TELEMETRY_SERVER_OWNER = "toolkit.telemetry.server_owner";
 | 
						|
const PREF_TELEMETRY_ENABLED = "toolkit.telemetry.enabled";
 | 
						|
const PREF_DEBUG_SLOW_SQL = "toolkit.telemetry.debugSlowSql";
 | 
						|
const PREF_SYMBOL_SERVER_URI = "profiler.symbolicationUrl";
 | 
						|
const DEFAULT_SYMBOL_SERVER_URI =
 | 
						|
  "https://symbolication.services.mozilla.com/symbolicate/v4";
 | 
						|
const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
 | 
						|
 | 
						|
// ms idle before applying the filter (allow uninterrupted typing)
 | 
						|
const FILTER_IDLE_TIMEOUT = 500;
 | 
						|
 | 
						|
const isWindows = Services.appinfo.OS == "WINNT";
 | 
						|
const EOL = isWindows ? "\r\n" : "\n";
 | 
						|
 | 
						|
// This is the ping object currently displayed in the page.
 | 
						|
var gPingData = null;
 | 
						|
 | 
						|
// Cached value of document's RTL mode
 | 
						|
var documentRTLMode = "";
 | 
						|
 | 
						|
/**
 | 
						|
 * Helper function for determining whether the document direction is RTL.
 | 
						|
 * Caches result of check on first invocation.
 | 
						|
 */
 | 
						|
function isRTL() {
 | 
						|
  if (!documentRTLMode) {
 | 
						|
    documentRTLMode = window.getComputedStyle(document.body).direction;
 | 
						|
  }
 | 
						|
  return documentRTLMode == "rtl";
 | 
						|
}
 | 
						|
 | 
						|
function isFlatArray(obj) {
 | 
						|
  if (!Array.isArray(obj)) {
 | 
						|
    return false;
 | 
						|
  }
 | 
						|
  return !obj.some(e => typeof e == "object");
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * This is a helper function for explodeObject.
 | 
						|
 */
 | 
						|
function flattenObject(obj, map, path, array) {
 | 
						|
  for (let k of Object.keys(obj)) {
 | 
						|
    let newPath = [...path, array ? "[" + k + "]" : k];
 | 
						|
    let v = obj[k];
 | 
						|
    if (!v || typeof v != "object") {
 | 
						|
      map.set(newPath.join("."), v);
 | 
						|
    } else if (isFlatArray(v)) {
 | 
						|
      map.set(newPath.join("."), "[" + v.join(", ") + "]");
 | 
						|
    } else {
 | 
						|
      flattenObject(v, map, newPath, Array.isArray(v));
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * This turns a JSON object into a "flat" stringified form.
 | 
						|
 *
 | 
						|
 * For an object like {a: "1", b: {c: "2", d: "3"}} it returns a Map of the
 | 
						|
 * form Map(["a","1"], ["b.c", "2"], ["b.d", "3"]).
 | 
						|
 */
 | 
						|
function explodeObject(obj) {
 | 
						|
  let map = new Map();
 | 
						|
  flattenObject(obj, map, []);
 | 
						|
  return map;
 | 
						|
}
 | 
						|
 | 
						|
function filterObject(obj, filterOut) {
 | 
						|
  let ret = {};
 | 
						|
  for (let k of Object.keys(obj)) {
 | 
						|
    if (!filterOut.includes(k)) {
 | 
						|
      ret[k] = obj[k];
 | 
						|
    }
 | 
						|
  }
 | 
						|
  return ret;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * This turns a JSON object into a "flat" stringified form, separated into top-level sections.
 | 
						|
 *
 | 
						|
 * For an object like:
 | 
						|
 *   {
 | 
						|
 *     a: {b: "1"},
 | 
						|
 *     c: {d: "2", e: {f: "3"}}
 | 
						|
 *   }
 | 
						|
 * it returns a Map of the form:
 | 
						|
 *   Map([
 | 
						|
 *     ["a", Map(["b","1"])],
 | 
						|
 *     ["c", Map([["d", "2"], ["e.f", "3"]])]
 | 
						|
 *   ])
 | 
						|
 */
 | 
						|
function sectionalizeObject(obj) {
 | 
						|
  let map = new Map();
 | 
						|
  for (let k of Object.keys(obj)) {
 | 
						|
    map.set(k, explodeObject(obj[k]));
 | 
						|
  }
 | 
						|
  return map;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Obtain the main DOMWindow for the current context.
 | 
						|
 */
 | 
						|
function getMainWindow() {
 | 
						|
  return window.browsingContext.topChromeWindow;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Obtain the DOMWindow that can open a preferences pane.
 | 
						|
 *
 | 
						|
 * This is essentially "get the browser chrome window" with the added check
 | 
						|
 * that the supposed browser chrome window is capable of opening a preferences
 | 
						|
 * pane.
 | 
						|
 *
 | 
						|
 * This may return null if we can't find the browser chrome window.
 | 
						|
 */
 | 
						|
function getMainWindowWithPreferencesPane() {
 | 
						|
  let mainWindow = getMainWindow();
 | 
						|
  if (mainWindow && "openPreferences" in mainWindow) {
 | 
						|
    return mainWindow;
 | 
						|
  }
 | 
						|
  return null;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Remove all child nodes of a document node.
 | 
						|
 */
 | 
						|
function removeAllChildNodes(node) {
 | 
						|
  while (node.hasChildNodes()) {
 | 
						|
    node.removeChild(node.lastChild);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
var Settings = {
 | 
						|
  attachObservers() {
 | 
						|
    let elements = document.getElementsByClassName("change-data-choices-link");
 | 
						|
    for (let el of elements) {
 | 
						|
      el.parentElement.addEventListener("click", function (event) {
 | 
						|
        if (event.target.localName === "a") {
 | 
						|
          if (AppConstants.platform == "android") {
 | 
						|
            var { EventDispatcher } = ChromeUtils.importESModule(
 | 
						|
              "resource://gre/modules/Messaging.sys.mjs"
 | 
						|
            );
 | 
						|
            EventDispatcher.instance.sendRequest({
 | 
						|
              type: "Settings:Show",
 | 
						|
              resource: "preferences_privacy",
 | 
						|
            });
 | 
						|
          } else {
 | 
						|
            // Show the data choices preferences on desktop.
 | 
						|
            let mainWindow = getMainWindowWithPreferencesPane();
 | 
						|
            mainWindow.openPreferences("privacy-reports");
 | 
						|
          }
 | 
						|
        }
 | 
						|
      });
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Updates the button & text at the top of the page to reflect Telemetry state.
 | 
						|
   */
 | 
						|
  render() {
 | 
						|
    let settingsExplanation = document.getElementById("settings-explanation");
 | 
						|
    let extendedEnabled = Services.telemetry.canRecordExtended;
 | 
						|
 | 
						|
    let channel = extendedEnabled ? "prerelease" : "release";
 | 
						|
    let uploadcase = TelemetrySend.sendingEnabled() ? "enabled" : "disabled";
 | 
						|
 | 
						|
    document.l10n.setAttributes(
 | 
						|
      settingsExplanation,
 | 
						|
      "about-telemetry-settings-explanation",
 | 
						|
      { channel, uploadcase }
 | 
						|
    );
 | 
						|
 | 
						|
    this.attachObservers();
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
var PingPicker = {
 | 
						|
  viewCurrentPingData: null,
 | 
						|
  _archivedPings: null,
 | 
						|
  TYPE_ALL: "all",
 | 
						|
 | 
						|
  attachObservers() {
 | 
						|
    let pingSourceElements = document.getElementsByName("choose-ping-source");
 | 
						|
    for (let el of pingSourceElements) {
 | 
						|
      el.addEventListener("change", () => this.onPingSourceChanged());
 | 
						|
    }
 | 
						|
 | 
						|
    let displays = document.getElementsByName("choose-ping-display");
 | 
						|
    for (let el of displays) {
 | 
						|
      el.addEventListener("change", () => this.onPingDisplayChanged());
 | 
						|
    }
 | 
						|
 | 
						|
    document
 | 
						|
      .getElementById("show-subsession-data")
 | 
						|
      .addEventListener("change", () => {
 | 
						|
        this._updateCurrentPingData();
 | 
						|
      });
 | 
						|
 | 
						|
    document.getElementById("choose-ping-id").addEventListener("change", () => {
 | 
						|
      this._updateArchivedPingData();
 | 
						|
    });
 | 
						|
    document
 | 
						|
      .getElementById("choose-ping-type")
 | 
						|
      .addEventListener("change", () => {
 | 
						|
        this.filterDisplayedPings();
 | 
						|
      });
 | 
						|
 | 
						|
    document
 | 
						|
      .getElementById("newer-ping")
 | 
						|
      .addEventListener("click", () => this._movePingIndex(-1));
 | 
						|
    document
 | 
						|
      .getElementById("older-ping")
 | 
						|
      .addEventListener("click", () => this._movePingIndex(1));
 | 
						|
 | 
						|
    let pingPickerNeedHide = false;
 | 
						|
    let pingPicker = document.getElementById("ping-picker");
 | 
						|
    pingPicker.addEventListener(
 | 
						|
      "mouseenter",
 | 
						|
      () => (pingPickerNeedHide = false)
 | 
						|
    );
 | 
						|
    pingPicker.addEventListener(
 | 
						|
      "mouseleave",
 | 
						|
      () => (pingPickerNeedHide = true)
 | 
						|
    );
 | 
						|
    document.addEventListener("click", () => {
 | 
						|
      if (pingPickerNeedHide) {
 | 
						|
        pingPicker.classList.add("hidden");
 | 
						|
      }
 | 
						|
    });
 | 
						|
    document
 | 
						|
      .getElementById("stores")
 | 
						|
      .addEventListener("change", () => displayPingData(gPingData));
 | 
						|
    Array.from(document.querySelectorAll(".change-ping")).forEach(el => {
 | 
						|
      el.addEventListener("click", event => {
 | 
						|
        if (!pingPicker.classList.contains("hidden")) {
 | 
						|
          pingPicker.classList.add("hidden");
 | 
						|
        } else {
 | 
						|
          pingPicker.classList.remove("hidden");
 | 
						|
          event.stopPropagation();
 | 
						|
        }
 | 
						|
      });
 | 
						|
    });
 | 
						|
  },
 | 
						|
 | 
						|
  onPingSourceChanged() {
 | 
						|
    this.update();
 | 
						|
  },
 | 
						|
 | 
						|
  onPingDisplayChanged() {
 | 
						|
    this.update();
 | 
						|
  },
 | 
						|
 | 
						|
  render() {
 | 
						|
    // Display the type and controls if the ping is not current
 | 
						|
    let pingDate = document.getElementById("ping-date");
 | 
						|
    let pingType = document.getElementById("ping-type");
 | 
						|
    let controls = document.getElementById("controls");
 | 
						|
    let pingExplanation = document.getElementById("ping-explanation");
 | 
						|
 | 
						|
    if (!this.viewCurrentPingData) {
 | 
						|
      let pingName = this._getSelectedPingName();
 | 
						|
      // Change sidebar heading text.
 | 
						|
      pingDate.textContent = pingName;
 | 
						|
      pingDate.setAttribute("title", pingName);
 | 
						|
      let pingTypeText = this._getSelectedPingType();
 | 
						|
      controls.classList.remove("hidden");
 | 
						|
      pingType.textContent = pingTypeText;
 | 
						|
      document.l10n.setAttributes(
 | 
						|
        pingExplanation,
 | 
						|
        "about-telemetry-ping-details",
 | 
						|
        { timestamp: pingTypeText, name: pingName }
 | 
						|
      );
 | 
						|
    } else {
 | 
						|
      // Change sidebar heading text.
 | 
						|
      controls.classList.add("hidden");
 | 
						|
      document.l10n.setAttributes(
 | 
						|
        pingType,
 | 
						|
        "about-telemetry-current-data-sidebar"
 | 
						|
      );
 | 
						|
      // Change home page text.
 | 
						|
      document.l10n.setAttributes(
 | 
						|
        pingExplanation,
 | 
						|
        "about-telemetry-data-details-current"
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    GenericSubsection.deleteAllSubSections();
 | 
						|
  },
 | 
						|
 | 
						|
  async update() {
 | 
						|
    let viewCurrent = document.getElementById("ping-source-current").checked;
 | 
						|
    let currentChanged = viewCurrent !== this.viewCurrentPingData;
 | 
						|
    this.viewCurrentPingData = viewCurrent;
 | 
						|
 | 
						|
    // If we have no archived pings, disable the ping archive selection.
 | 
						|
    // This can happen on new profiles or if the ping archive is disabled.
 | 
						|
    let archivedPingList = await TelemetryArchive.promiseArchivedPingList();
 | 
						|
    let sourceArchived = document.getElementById("ping-source-archive");
 | 
						|
    let sourceArchivedContainer = document.getElementById(
 | 
						|
      "ping-source-archive-container"
 | 
						|
    );
 | 
						|
    let archivedDisabled = !archivedPingList.length;
 | 
						|
    sourceArchived.disabled = archivedDisabled;
 | 
						|
    sourceArchivedContainer.classList.toggle("disabled", archivedDisabled);
 | 
						|
 | 
						|
    if (currentChanged) {
 | 
						|
      if (this.viewCurrentPingData) {
 | 
						|
        document.getElementById("current-ping-picker").hidden = false;
 | 
						|
        document.getElementById("archived-ping-picker").hidden = true;
 | 
						|
        this._updateCurrentPingData();
 | 
						|
      } else {
 | 
						|
        document.getElementById("current-ping-picker").hidden = true;
 | 
						|
        await this._updateArchivedPingList(archivedPingList);
 | 
						|
        document.getElementById("archived-ping-picker").hidden = false;
 | 
						|
      }
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  _updateCurrentPingData() {
 | 
						|
    TelemetryController.ensureInitialized().then(() =>
 | 
						|
      this._doUpdateCurrentPingData()
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  _doUpdateCurrentPingData() {
 | 
						|
    const subsession = document.getElementById("show-subsession-data").checked;
 | 
						|
    let ping = TelemetryController.getCurrentPingData(subsession);
 | 
						|
    if (!ping) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    let stores = Telemetry.getAllStores();
 | 
						|
    let getData = {
 | 
						|
      histograms: Telemetry.getSnapshotForHistograms,
 | 
						|
      keyedHistograms: Telemetry.getSnapshotForKeyedHistograms,
 | 
						|
      scalars: Telemetry.getSnapshotForScalars,
 | 
						|
      keyedScalars: Telemetry.getSnapshotForKeyedScalars,
 | 
						|
    };
 | 
						|
 | 
						|
    let data = {};
 | 
						|
    for (const [name, fn] of Object.entries(getData)) {
 | 
						|
      for (const store of stores) {
 | 
						|
        if (!data[store]) {
 | 
						|
          data[store] = {};
 | 
						|
        }
 | 
						|
        let measurement = fn(store, /* clear */ false, /* filterTest */ true);
 | 
						|
        let processes = Object.keys(measurement);
 | 
						|
 | 
						|
        for (const process of processes) {
 | 
						|
          if (!data[store][process]) {
 | 
						|
            data[store][process] = {};
 | 
						|
          }
 | 
						|
 | 
						|
          data[store][process][name] = measurement[process];
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
    ping.payload.stores = data;
 | 
						|
 | 
						|
    // Delete the unused data from the payload of the current ping.
 | 
						|
    // It's included in the above `stores` attribute.
 | 
						|
    for (const data of Object.values(ping.payload.processes)) {
 | 
						|
      delete data.scalars;
 | 
						|
      delete data.keyedScalars;
 | 
						|
      delete data.histograms;
 | 
						|
      delete data.keyedHistograms;
 | 
						|
    }
 | 
						|
    delete ping.payload.histograms;
 | 
						|
    delete ping.payload.keyedHistograms;
 | 
						|
 | 
						|
    // augment ping payload with event telemetry
 | 
						|
    let eventSnapshot = Telemetry.snapshotEvents(
 | 
						|
      Telemetry.DATASET_PRERELEASE_CHANNELS,
 | 
						|
      false
 | 
						|
    );
 | 
						|
    for (let process of Object.keys(eventSnapshot)) {
 | 
						|
      if (process in ping.payload.processes) {
 | 
						|
        ping.payload.processes[process].events = eventSnapshot[process].filter(
 | 
						|
          e => !e[1].startsWith("telemetry.test")
 | 
						|
        );
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    displayPingData(ping, true);
 | 
						|
  },
 | 
						|
 | 
						|
  _updateArchivedPingData() {
 | 
						|
    let id = this._getSelectedPingId();
 | 
						|
    let res = Promise.resolve();
 | 
						|
    if (id) {
 | 
						|
      res = TelemetryArchive.promiseArchivedPingById(id).then(ping =>
 | 
						|
        displayPingData(ping, true)
 | 
						|
      );
 | 
						|
    }
 | 
						|
    return res;
 | 
						|
  },
 | 
						|
 | 
						|
  async _updateArchivedPingList(pingList) {
 | 
						|
    // The archived ping list is sorted in ascending timestamp order,
 | 
						|
    // but descending is more practical for the operations we do here.
 | 
						|
    pingList.reverse();
 | 
						|
    this._archivedPings = pingList;
 | 
						|
    // Render the archive data.
 | 
						|
    this._renderPingList();
 | 
						|
    // Update the displayed ping.
 | 
						|
    await this._updateArchivedPingData();
 | 
						|
  },
 | 
						|
 | 
						|
  _renderPingList() {
 | 
						|
    let pingSelector = document.getElementById("choose-ping-id");
 | 
						|
    Array.from(pingSelector.children).forEach(child =>
 | 
						|
      removeAllChildNodes(child)
 | 
						|
    );
 | 
						|
 | 
						|
    let pingTypes = new Set();
 | 
						|
    pingTypes.add(this.TYPE_ALL);
 | 
						|
 | 
						|
    const today = new Date();
 | 
						|
    today.setHours(0, 0, 0, 0);
 | 
						|
    const yesterday = new Date(today);
 | 
						|
    yesterday.setDate(today.getDate() - 1);
 | 
						|
 | 
						|
    for (let p of this._archivedPings) {
 | 
						|
      pingTypes.add(p.type);
 | 
						|
      const pingDate = new Date(p.timestampCreated);
 | 
						|
      const datetimeText = new Services.intl.DateTimeFormat(undefined, {
 | 
						|
        dateStyle: "short",
 | 
						|
        timeStyle: "medium",
 | 
						|
      }).format(pingDate);
 | 
						|
      const pingName = `${datetimeText}, ${p.type}`;
 | 
						|
 | 
						|
      let option = document.createElement("option");
 | 
						|
      let content = document.createTextNode(pingName);
 | 
						|
      option.appendChild(content);
 | 
						|
      option.setAttribute("value", p.id);
 | 
						|
      option.dataset.type = p.type;
 | 
						|
      option.dataset.date = datetimeText;
 | 
						|
 | 
						|
      pingDate.setHours(0, 0, 0, 0);
 | 
						|
      if (pingDate.getTime() === today.getTime()) {
 | 
						|
        pingSelector.children[0].appendChild(option);
 | 
						|
      } else if (pingDate.getTime() === yesterday.getTime()) {
 | 
						|
        pingSelector.children[1].appendChild(option);
 | 
						|
      } else {
 | 
						|
        pingSelector.children[2].appendChild(option);
 | 
						|
      }
 | 
						|
    }
 | 
						|
    this._renderPingTypes(pingTypes);
 | 
						|
  },
 | 
						|
 | 
						|
  _renderPingTypes(pingTypes) {
 | 
						|
    let pingTypeSelector = document.getElementById("choose-ping-type");
 | 
						|
    removeAllChildNodes(pingTypeSelector);
 | 
						|
    pingTypes.forEach(type => {
 | 
						|
      let option = document.createElement("option");
 | 
						|
      option.appendChild(document.createTextNode(type));
 | 
						|
      option.setAttribute("value", type);
 | 
						|
      pingTypeSelector.appendChild(option);
 | 
						|
    });
 | 
						|
  },
 | 
						|
 | 
						|
  _movePingIndex(offset) {
 | 
						|
    if (this.viewCurrentPingData) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    let typeSelector = document.getElementById("choose-ping-type");
 | 
						|
    let type = typeSelector.selectedOptions.item(0).value;
 | 
						|
 | 
						|
    let id = this._getSelectedPingId();
 | 
						|
    let index = this._archivedPings.findIndex(p => p.id == id);
 | 
						|
    let newIndex = Math.min(
 | 
						|
      Math.max(0, index + offset),
 | 
						|
      this._archivedPings.length - 1
 | 
						|
    );
 | 
						|
 | 
						|
    let pingList;
 | 
						|
    if (offset > 0) {
 | 
						|
      pingList = this._archivedPings.slice(newIndex);
 | 
						|
    } else {
 | 
						|
      pingList = this._archivedPings.slice(0, newIndex);
 | 
						|
      pingList.reverse();
 | 
						|
    }
 | 
						|
 | 
						|
    let ping = pingList.find(p => {
 | 
						|
      return type == this.TYPE_ALL || p.type == type;
 | 
						|
    });
 | 
						|
 | 
						|
    if (ping) {
 | 
						|
      this.selectPing(ping);
 | 
						|
      this._updateArchivedPingData();
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  selectPing(ping) {
 | 
						|
    let pingSelector = document.getElementById("choose-ping-id");
 | 
						|
    // Use some() to break if we find the ping.
 | 
						|
    Array.from(pingSelector.children).some(group => {
 | 
						|
      return Array.from(group.children).some(option => {
 | 
						|
        if (option.value == ping.id) {
 | 
						|
          option.selected = true;
 | 
						|
          return true;
 | 
						|
        }
 | 
						|
        return false;
 | 
						|
      });
 | 
						|
    });
 | 
						|
  },
 | 
						|
 | 
						|
  filterDisplayedPings() {
 | 
						|
    let pingSelector = document.getElementById("choose-ping-id");
 | 
						|
    let typeSelector = document.getElementById("choose-ping-type");
 | 
						|
    let type = typeSelector.selectedOptions.item(0).value;
 | 
						|
    let first = true;
 | 
						|
    Array.from(pingSelector.children).forEach(group => {
 | 
						|
      Array.from(group.children).forEach(option => {
 | 
						|
        if (first && option.dataset.type == type) {
 | 
						|
          option.selected = true;
 | 
						|
          first = false;
 | 
						|
        }
 | 
						|
        option.hidden = type != this.TYPE_ALL && option.dataset.type != type;
 | 
						|
        // Arrow keys should only iterate over visible options
 | 
						|
        option.disabled = option.hidden;
 | 
						|
      });
 | 
						|
    });
 | 
						|
    this._updateArchivedPingData();
 | 
						|
  },
 | 
						|
 | 
						|
  _getSelectedPingName() {
 | 
						|
    let pingSelector = document.getElementById("choose-ping-id");
 | 
						|
    let selected = pingSelector.selectedOptions.item(0);
 | 
						|
    return selected.dataset.date;
 | 
						|
  },
 | 
						|
 | 
						|
  _getSelectedPingType() {
 | 
						|
    let pingSelector = document.getElementById("choose-ping-id");
 | 
						|
    let selected = pingSelector.selectedOptions.item(0);
 | 
						|
    return selected.dataset.type;
 | 
						|
  },
 | 
						|
 | 
						|
  _getSelectedPingId() {
 | 
						|
    let pingSelector = document.getElementById("choose-ping-id");
 | 
						|
    let selected = pingSelector.selectedOptions.item(0);
 | 
						|
    return selected.getAttribute("value");
 | 
						|
  },
 | 
						|
 | 
						|
  _showRawPingData() {
 | 
						|
    show(document.getElementById("category-raw"));
 | 
						|
  },
 | 
						|
 | 
						|
  _showStructuredPingData() {
 | 
						|
    show(document.getElementById("category-home"));
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
var GeneralData = {
 | 
						|
  /**
 | 
						|
   * Renders the general data
 | 
						|
   */
 | 
						|
  render(aPing) {
 | 
						|
    setHasData("general-data-section", true);
 | 
						|
    let generalDataSection = document.getElementById("general-data");
 | 
						|
    removeAllChildNodes(generalDataSection);
 | 
						|
 | 
						|
    const headings = [
 | 
						|
      "about-telemetry-names-header",
 | 
						|
      "about-telemetry-values-header",
 | 
						|
    ];
 | 
						|
 | 
						|
    // The payload & environment parts are handled by other renderers.
 | 
						|
    let ignoreSections = ["payload", "environment"];
 | 
						|
    let data = explodeObject(filterObject(aPing, ignoreSections));
 | 
						|
 | 
						|
    const table = GenericTable.render(data, headings);
 | 
						|
    generalDataSection.appendChild(table);
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
var EnvironmentData = {
 | 
						|
  /**
 | 
						|
   * Renders the environment data
 | 
						|
   */
 | 
						|
  render(ping) {
 | 
						|
    let dataDiv = document.getElementById("environment-data");
 | 
						|
    removeAllChildNodes(dataDiv);
 | 
						|
    const hasData = !!ping.environment;
 | 
						|
    setHasData("environment-data-section", hasData);
 | 
						|
    if (!hasData) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    let ignore = ["addons"];
 | 
						|
    let env = filterObject(ping.environment, ignore);
 | 
						|
    let sections = sectionalizeObject(env);
 | 
						|
    GenericSubsection.render(sections, dataDiv, "environment-data-section");
 | 
						|
 | 
						|
    // We use specialized rendering here to make the addon and plugin listings
 | 
						|
    // more readable.
 | 
						|
    this.createAddonSection(dataDiv, ping);
 | 
						|
  },
 | 
						|
 | 
						|
  renderAddonsObject(addonObj, addonSection, sectionTitle) {
 | 
						|
    let table = document.createElement("table");
 | 
						|
    table.setAttribute("id", sectionTitle);
 | 
						|
    this.appendAddonSubsectionTitle(sectionTitle, table);
 | 
						|
 | 
						|
    for (let id of Object.keys(addonObj)) {
 | 
						|
      let addon = addonObj[id];
 | 
						|
      this.appendHeadingName(table, addon.name || id);
 | 
						|
      this.appendAddonID(table, id);
 | 
						|
      let data = explodeObject(addon);
 | 
						|
 | 
						|
      for (let [key, value] of data) {
 | 
						|
        this.appendRow(table, key, value);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    addonSection.appendChild(table);
 | 
						|
  },
 | 
						|
 | 
						|
  renderKeyValueObject(addonObj, addonSection, sectionTitle) {
 | 
						|
    let data = explodeObject(addonObj);
 | 
						|
    let table = GenericTable.render(data);
 | 
						|
    table.setAttribute("class", sectionTitle);
 | 
						|
    this.appendAddonSubsectionTitle(sectionTitle, table);
 | 
						|
    addonSection.appendChild(table);
 | 
						|
  },
 | 
						|
 | 
						|
  appendAddonID(table, addonID) {
 | 
						|
    this.appendRow(table, "id", addonID);
 | 
						|
  },
 | 
						|
 | 
						|
  appendHeadingName(table, name) {
 | 
						|
    let headings = document.createElement("tr");
 | 
						|
    this.appendColumn(headings, "th", name);
 | 
						|
    headings.cells[0].colSpan = 2;
 | 
						|
    table.appendChild(headings);
 | 
						|
  },
 | 
						|
 | 
						|
  appendAddonSubsectionTitle(section, table) {
 | 
						|
    let caption = document.createElement("caption");
 | 
						|
    caption.appendChild(document.createTextNode(section));
 | 
						|
    table.appendChild(caption);
 | 
						|
  },
 | 
						|
 | 
						|
  createAddonSection(dataDiv, ping) {
 | 
						|
    if (!ping || !("environment" in ping) || !("addons" in ping.environment)) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    let addonSection = document.createElement("div");
 | 
						|
    addonSection.setAttribute("class", "subsection-data subdata");
 | 
						|
    let addons = ping.environment.addons;
 | 
						|
    this.renderAddonsObject(addons.activeAddons, addonSection, "activeAddons");
 | 
						|
    this.renderKeyValueObject(addons.theme, addonSection, "theme");
 | 
						|
    this.renderAddonsObject(
 | 
						|
      addons.activeGMPlugins,
 | 
						|
      addonSection,
 | 
						|
      "activeGMPlugins"
 | 
						|
    );
 | 
						|
 | 
						|
    let hasAddonData = !!Object.keys(ping.environment.addons).length;
 | 
						|
    let s = GenericSubsection.renderSubsectionHeader(
 | 
						|
      "addons",
 | 
						|
      hasAddonData,
 | 
						|
      "environment-data-section"
 | 
						|
    );
 | 
						|
    s.appendChild(addonSection);
 | 
						|
    dataDiv.appendChild(s);
 | 
						|
  },
 | 
						|
 | 
						|
  appendRow(table, id, value) {
 | 
						|
    let row = document.createElement("tr");
 | 
						|
    row.id = id;
 | 
						|
    this.appendColumn(row, "td", id);
 | 
						|
    this.appendColumn(row, "td", value);
 | 
						|
    table.appendChild(row);
 | 
						|
  },
 | 
						|
  /**
 | 
						|
   * Helper function for appending a column to the data table.
 | 
						|
   *
 | 
						|
   * @param aRowElement Parent row element
 | 
						|
   * @param aColType Column's tag name
 | 
						|
   * @param aColText Column contents
 | 
						|
   */
 | 
						|
  appendColumn(aRowElement, aColType, aColText) {
 | 
						|
    let colElement = document.createElement(aColType);
 | 
						|
    let colTextElement = document.createTextNode(aColText);
 | 
						|
    colElement.appendChild(colTextElement);
 | 
						|
    aRowElement.appendChild(colElement);
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
var SlowSQL = {
 | 
						|
  /**
 | 
						|
   * Render slow SQL statistics
 | 
						|
   */
 | 
						|
  render: function SlowSQL_render(aPing) {
 | 
						|
    // We can add the debug SQL data to the current ping later.
 | 
						|
    // However, we need to be careful to never send that debug data
 | 
						|
    // out due to privacy concerns.
 | 
						|
    // We want to show the actual ping data for archived pings,
 | 
						|
    // so skip this there.
 | 
						|
 | 
						|
    let debugSlowSql =
 | 
						|
      PingPicker.viewCurrentPingData &&
 | 
						|
      Preferences.get(PREF_DEBUG_SLOW_SQL, false);
 | 
						|
    let slowSql = debugSlowSql ? Telemetry.debugSlowSQL : aPing.payload.slowSQL;
 | 
						|
    if (!slowSql) {
 | 
						|
      setHasData("slow-sql-section", false);
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    let { mainThread, otherThreads } = debugSlowSql
 | 
						|
      ? Telemetry.debugSlowSQL
 | 
						|
      : aPing.payload.slowSQL;
 | 
						|
 | 
						|
    let mainThreadCount = Object.keys(mainThread).length;
 | 
						|
    let otherThreadCount = Object.keys(otherThreads).length;
 | 
						|
    if (mainThreadCount == 0 && otherThreadCount == 0) {
 | 
						|
      setHasData("slow-sql-section", false);
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    setHasData("slow-sql-section", true);
 | 
						|
    if (debugSlowSql) {
 | 
						|
      document.getElementById("sql-warning").hidden = false;
 | 
						|
    }
 | 
						|
 | 
						|
    let slowSqlDiv = document.getElementById("slow-sql-tables");
 | 
						|
    removeAllChildNodes(slowSqlDiv);
 | 
						|
 | 
						|
    // Main thread
 | 
						|
    if (mainThreadCount > 0) {
 | 
						|
      let table = document.createElement("table");
 | 
						|
      this.renderTableHeader(table, "main");
 | 
						|
      this.renderTable(table, mainThread);
 | 
						|
      slowSqlDiv.appendChild(table);
 | 
						|
    }
 | 
						|
 | 
						|
    // Other threads
 | 
						|
    if (otherThreadCount > 0) {
 | 
						|
      let table = document.createElement("table");
 | 
						|
      this.renderTableHeader(table, "other");
 | 
						|
      this.renderTable(table, otherThreads);
 | 
						|
      slowSqlDiv.appendChild(table);
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Creates a header row for a Slow SQL table
 | 
						|
   * Tabs & newlines added to cells to make it easier to copy-paste.
 | 
						|
   *
 | 
						|
   * @param aTable Parent table element
 | 
						|
   * @param aTitle Table's title
 | 
						|
   */
 | 
						|
  renderTableHeader: function SlowSQL_renderTableHeader(aTable, threadType) {
 | 
						|
    let caption = document.createElement("caption");
 | 
						|
    if (threadType == "main") {
 | 
						|
      document.l10n.setAttributes(caption, "about-telemetry-slow-sql-main");
 | 
						|
    }
 | 
						|
 | 
						|
    if (threadType == "other") {
 | 
						|
      document.l10n.setAttributes(caption, "about-telemetry-slow-sql-other");
 | 
						|
    }
 | 
						|
    aTable.appendChild(caption);
 | 
						|
 | 
						|
    let headings = document.createElement("tr");
 | 
						|
    document.l10n.setAttributes(
 | 
						|
      this.appendColumn(headings, "th"),
 | 
						|
      "about-telemetry-slow-sql-hits"
 | 
						|
    );
 | 
						|
    document.l10n.setAttributes(
 | 
						|
      this.appendColumn(headings, "th"),
 | 
						|
      "about-telemetry-slow-sql-average"
 | 
						|
    );
 | 
						|
    document.l10n.setAttributes(
 | 
						|
      this.appendColumn(headings, "th"),
 | 
						|
      "about-telemetry-slow-sql-statement"
 | 
						|
    );
 | 
						|
    aTable.appendChild(headings);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Fills out the table body
 | 
						|
   * Tabs & newlines added to cells to make it easier to copy-paste.
 | 
						|
   *
 | 
						|
   * @param aTable Parent table element
 | 
						|
   * @param aSql SQL stats object
 | 
						|
   */
 | 
						|
  renderTable: function SlowSQL_renderTable(aTable, aSql) {
 | 
						|
    for (let [sql, [hitCount, totalTime]] of Object.entries(aSql)) {
 | 
						|
      let averageTime = totalTime / hitCount;
 | 
						|
 | 
						|
      let sqlRow = document.createElement("tr");
 | 
						|
 | 
						|
      this.appendColumn(sqlRow, "td", hitCount + "\t");
 | 
						|
      this.appendColumn(sqlRow, "td", averageTime.toFixed(0) + "\t");
 | 
						|
      this.appendColumn(sqlRow, "td", sql + "\n");
 | 
						|
 | 
						|
      aTable.appendChild(sqlRow);
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Helper function for appending a column to a Slow SQL table.
 | 
						|
   *
 | 
						|
   * @param aRowElement Parent row element
 | 
						|
   * @param aColType Column's tag name
 | 
						|
   * @param aColText Column contents
 | 
						|
   */
 | 
						|
  appendColumn: function SlowSQL_appendColumn(
 | 
						|
    aRowElement,
 | 
						|
    aColType,
 | 
						|
    aColText = ""
 | 
						|
  ) {
 | 
						|
    let colElement = document.createElement(aColType);
 | 
						|
    if (aColText) {
 | 
						|
      let colTextElement = document.createTextNode(aColText);
 | 
						|
      colElement.appendChild(colTextElement);
 | 
						|
    }
 | 
						|
    aRowElement.appendChild(colElement);
 | 
						|
    return colElement;
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
var StackRenderer = {
 | 
						|
  /**
 | 
						|
   * Outputs the memory map associated with this hang report
 | 
						|
   *
 | 
						|
   * @param aDiv Output div
 | 
						|
   */
 | 
						|
  renderMemoryMap: async function StackRenderer_renderMemoryMap(
 | 
						|
    aDiv,
 | 
						|
    memoryMap
 | 
						|
  ) {
 | 
						|
    let memoryMapTitleElement = document.createElement("span");
 | 
						|
    document.l10n.setAttributes(
 | 
						|
      memoryMapTitleElement,
 | 
						|
      "about-telemetry-memory-map-title"
 | 
						|
    );
 | 
						|
    aDiv.appendChild(memoryMapTitleElement);
 | 
						|
    aDiv.appendChild(document.createElement("br"));
 | 
						|
 | 
						|
    for (let currentModule of memoryMap) {
 | 
						|
      aDiv.appendChild(document.createTextNode(currentModule.join(" ")));
 | 
						|
      aDiv.appendChild(document.createElement("br"));
 | 
						|
    }
 | 
						|
 | 
						|
    aDiv.appendChild(document.createElement("br"));
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Outputs the raw PCs from the hang's stack
 | 
						|
   *
 | 
						|
   * @param aDiv Output div
 | 
						|
   * @param aStack Array of PCs from the hang stack
 | 
						|
   */
 | 
						|
  renderStack: function StackRenderer_renderStack(aDiv, aStack) {
 | 
						|
    let stackTitleElement = document.createElement("span");
 | 
						|
    document.l10n.setAttributes(
 | 
						|
      stackTitleElement,
 | 
						|
      "about-telemetry-stack-title"
 | 
						|
    );
 | 
						|
    aDiv.appendChild(stackTitleElement);
 | 
						|
    let stackText = " " + aStack.join(" ");
 | 
						|
    aDiv.appendChild(document.createTextNode(stackText));
 | 
						|
 | 
						|
    aDiv.appendChild(document.createElement("br"));
 | 
						|
    aDiv.appendChild(document.createElement("br"));
 | 
						|
  },
 | 
						|
  renderStacks: function StackRenderer_renderStacks(
 | 
						|
    aPrefix,
 | 
						|
    aStacks,
 | 
						|
    aMemoryMap,
 | 
						|
    aRenderHeader
 | 
						|
  ) {
 | 
						|
    let div = document.getElementById(aPrefix);
 | 
						|
    removeAllChildNodes(div);
 | 
						|
 | 
						|
    let fetchE = document.getElementById(aPrefix + "-fetch-symbols");
 | 
						|
    if (fetchE) {
 | 
						|
      fetchE.hidden = false;
 | 
						|
    }
 | 
						|
    let hideE = document.getElementById(aPrefix + "-hide-symbols");
 | 
						|
    if (hideE) {
 | 
						|
      hideE.hidden = true;
 | 
						|
    }
 | 
						|
 | 
						|
    if (!aStacks.length) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    setHasData(aPrefix + "-section", true);
 | 
						|
 | 
						|
    this.renderMemoryMap(div, aMemoryMap);
 | 
						|
 | 
						|
    for (let i = 0; i < aStacks.length; ++i) {
 | 
						|
      let stack = aStacks[i];
 | 
						|
      aRenderHeader(i);
 | 
						|
      this.renderStack(div, stack);
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Renders the title of the stack: e.g. "Late Write #1" or
 | 
						|
   * "Hang Report #1 (6 seconds)".
 | 
						|
   *
 | 
						|
   * @param aDivId The id of the div to append the header to.
 | 
						|
   * @param aL10nId The l10n id of the message to use for the title.
 | 
						|
   * @param aL10nArgs The l10n args for the provided message id.
 | 
						|
   */
 | 
						|
  renderHeader: function StackRenderer_renderHeader(
 | 
						|
    aDivId,
 | 
						|
    aL10nId,
 | 
						|
    aL10nArgs
 | 
						|
  ) {
 | 
						|
    let div = document.getElementById(aDivId);
 | 
						|
 | 
						|
    let titleElement = document.createElement("span");
 | 
						|
    titleElement.className = "stack-title";
 | 
						|
 | 
						|
    document.l10n.setAttributes(titleElement, aL10nId, aL10nArgs);
 | 
						|
 | 
						|
    div.appendChild(titleElement);
 | 
						|
    div.appendChild(document.createElement("br"));
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
var RawPayloadData = {
 | 
						|
  /**
 | 
						|
   * Renders the raw pyaload.
 | 
						|
   */
 | 
						|
  render(aPing) {
 | 
						|
    setHasData("raw-payload-section", true);
 | 
						|
    let pre = document.getElementById("raw-payload-data");
 | 
						|
    pre.textContent = JSON.stringify(aPing.payload, null, 2);
 | 
						|
  },
 | 
						|
 | 
						|
  attachObservers() {
 | 
						|
    document
 | 
						|
      .getElementById("payload-json-viewer")
 | 
						|
      .addEventListener("click", () => {
 | 
						|
        openJsonInFirefoxJsonViewer(JSON.stringify(gPingData.payload, null, 2));
 | 
						|
      });
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
function SymbolicationRequest(
 | 
						|
  aPrefix,
 | 
						|
  aRenderHeader,
 | 
						|
  aMemoryMap,
 | 
						|
  aStacks,
 | 
						|
  aDurations = null
 | 
						|
) {
 | 
						|
  this.prefix = aPrefix;
 | 
						|
  this.renderHeader = aRenderHeader;
 | 
						|
  this.memoryMap = aMemoryMap;
 | 
						|
  this.stacks = aStacks;
 | 
						|
  this.durations = aDurations;
 | 
						|
}
 | 
						|
/**
 | 
						|
 * A callback for onreadystatechange. It replaces the numeric stack with
 | 
						|
 * the symbolicated one returned by the symbolication server.
 | 
						|
 */
 | 
						|
SymbolicationRequest.prototype.handleSymbolResponse =
 | 
						|
  async function SymbolicationRequest_handleSymbolResponse() {
 | 
						|
    if (this.symbolRequest.readyState != 4) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    let fetchElement = document.getElementById(this.prefix + "-fetch-symbols");
 | 
						|
    fetchElement.hidden = true;
 | 
						|
    let hideElement = document.getElementById(this.prefix + "-hide-symbols");
 | 
						|
    hideElement.hidden = false;
 | 
						|
    let div = document.getElementById(this.prefix);
 | 
						|
    removeAllChildNodes(div);
 | 
						|
    let errorMessage = await document.l10n.formatValue(
 | 
						|
      "about-telemetry-error-fetching-symbols"
 | 
						|
    );
 | 
						|
 | 
						|
    if (this.symbolRequest.status != 200) {
 | 
						|
      div.appendChild(document.createTextNode(errorMessage));
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    let jsonResponse = {};
 | 
						|
    try {
 | 
						|
      jsonResponse = JSON.parse(this.symbolRequest.responseText);
 | 
						|
    } catch (e) {
 | 
						|
      div.appendChild(document.createTextNode(errorMessage));
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    for (let i = 0; i < jsonResponse.length; ++i) {
 | 
						|
      let stack = jsonResponse[i];
 | 
						|
      this.renderHeader(i, this.durations);
 | 
						|
 | 
						|
      for (let symbol of stack) {
 | 
						|
        div.appendChild(document.createTextNode(symbol));
 | 
						|
        div.appendChild(document.createElement("br"));
 | 
						|
      }
 | 
						|
      div.appendChild(document.createElement("br"));
 | 
						|
    }
 | 
						|
  };
 | 
						|
/**
 | 
						|
 * Send a request to the symbolication server to symbolicate this stack.
 | 
						|
 */
 | 
						|
SymbolicationRequest.prototype.fetchSymbols =
 | 
						|
  function SymbolicationRequest_fetchSymbols() {
 | 
						|
    let symbolServerURI = Preferences.get(
 | 
						|
      PREF_SYMBOL_SERVER_URI,
 | 
						|
      DEFAULT_SYMBOL_SERVER_URI
 | 
						|
    );
 | 
						|
    let request = {
 | 
						|
      memoryMap: this.memoryMap,
 | 
						|
      stacks: this.stacks,
 | 
						|
      version: 3,
 | 
						|
    };
 | 
						|
    let requestJSON = JSON.stringify(request);
 | 
						|
 | 
						|
    this.symbolRequest = new XMLHttpRequest();
 | 
						|
    this.symbolRequest.open("POST", symbolServerURI, true);
 | 
						|
    this.symbolRequest.setRequestHeader("Content-type", "application/json");
 | 
						|
    this.symbolRequest.setRequestHeader("Content-length", requestJSON.length);
 | 
						|
    this.symbolRequest.setRequestHeader("Connection", "close");
 | 
						|
    this.symbolRequest.onreadystatechange =
 | 
						|
      this.handleSymbolResponse.bind(this);
 | 
						|
    this.symbolRequest.send(requestJSON);
 | 
						|
  };
 | 
						|
 | 
						|
var Histogram = {
 | 
						|
  /**
 | 
						|
   * Renders a single Telemetry histogram
 | 
						|
   *
 | 
						|
   * @param aParent Parent element
 | 
						|
   * @param aName Histogram name
 | 
						|
   * @param aHgram Histogram information
 | 
						|
   * @param aOptions Object with render options
 | 
						|
   *                 * exponential: bars follow logarithmic scale
 | 
						|
   */
 | 
						|
  render: function Histogram_render(aParent, aName, aHgram, aOptions) {
 | 
						|
    let options = aOptions || {};
 | 
						|
    let hgram = this.processHistogram(aHgram, aName);
 | 
						|
 | 
						|
    let outerDiv = document.createElement("div");
 | 
						|
    outerDiv.className = "histogram";
 | 
						|
    outerDiv.id = aName;
 | 
						|
 | 
						|
    let divTitle = document.createElement("div");
 | 
						|
    divTitle.classList.add("histogram-title");
 | 
						|
    divTitle.appendChild(document.createTextNode(aName));
 | 
						|
    outerDiv.appendChild(divTitle);
 | 
						|
 | 
						|
    let divStats = document.createElement("div");
 | 
						|
    divStats.classList.add("histogram-stats");
 | 
						|
 | 
						|
    let histogramStatsArgs = {
 | 
						|
      sampleCount: hgram.sample_count,
 | 
						|
      prettyAverage: hgram.pretty_average,
 | 
						|
      sum: hgram.sum,
 | 
						|
    };
 | 
						|
 | 
						|
    document.l10n.setAttributes(
 | 
						|
      divStats,
 | 
						|
      "about-telemetry-histogram-stats",
 | 
						|
      histogramStatsArgs
 | 
						|
    );
 | 
						|
 | 
						|
    if (isRTL()) {
 | 
						|
      hgram.values.reverse();
 | 
						|
    }
 | 
						|
 | 
						|
    let textData = this.renderValues(outerDiv, hgram, options);
 | 
						|
 | 
						|
    // The 'Copy' button contains the textual data, copied to clipboard on click
 | 
						|
    let copyButton = document.createElement("button");
 | 
						|
    copyButton.className = "copy-node";
 | 
						|
    document.l10n.setAttributes(copyButton, "about-telemetry-histogram-copy");
 | 
						|
 | 
						|
    copyButton.addEventListener("click", async function () {
 | 
						|
      let divStatsString = await document.l10n.formatValue(
 | 
						|
        "about-telemetry-histogram-stats",
 | 
						|
        histogramStatsArgs
 | 
						|
      );
 | 
						|
      copyButton.histogramText =
 | 
						|
        aName + EOL + divStatsString + EOL + EOL + textData;
 | 
						|
      Cc["@mozilla.org/widget/clipboardhelper;1"]
 | 
						|
        .getService(Ci.nsIClipboardHelper)
 | 
						|
        .copyString(this.histogramText);
 | 
						|
    });
 | 
						|
    outerDiv.appendChild(copyButton);
 | 
						|
 | 
						|
    aParent.appendChild(outerDiv);
 | 
						|
    return outerDiv;
 | 
						|
  },
 | 
						|
 | 
						|
  processHistogram(aHgram) {
 | 
						|
    const values = Object.keys(aHgram.values).map(k => aHgram.values[k]);
 | 
						|
    if (!values.length) {
 | 
						|
      // If we have no values collected for this histogram, just return
 | 
						|
      // zero values so we still render it.
 | 
						|
      return {
 | 
						|
        values: [],
 | 
						|
        pretty_average: 0,
 | 
						|
        max: 0,
 | 
						|
        sample_count: 0,
 | 
						|
        sum: 0,
 | 
						|
      };
 | 
						|
    }
 | 
						|
 | 
						|
    const sample_count = values.reduceRight((a, b) => a + b);
 | 
						|
    const average = Math.round((aHgram.sum * 10) / sample_count) / 10;
 | 
						|
    const max_value = Math.max(...values);
 | 
						|
 | 
						|
    const labelledValues = Object.keys(aHgram.values).map(k => [
 | 
						|
      Number(k),
 | 
						|
      aHgram.values[k],
 | 
						|
    ]);
 | 
						|
 | 
						|
    let result = {
 | 
						|
      values: labelledValues,
 | 
						|
      pretty_average: average,
 | 
						|
      max: max_value,
 | 
						|
      sample_count,
 | 
						|
      sum: aHgram.sum,
 | 
						|
    };
 | 
						|
 | 
						|
    return result;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Return a non-negative, logarithmic representation of a non-negative number.
 | 
						|
   * e.g. 0 => 0, 1 => 1, 10 => 2, 100 => 3
 | 
						|
   *
 | 
						|
   * @param aNumber Non-negative number
 | 
						|
   */
 | 
						|
  getLogValue(aNumber) {
 | 
						|
    return Math.max(0, Math.log10(aNumber) + 1);
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Create histogram HTML bars, also returns a textual representation
 | 
						|
   * Both aMaxValue and aSumValues must be positive.
 | 
						|
   * Values are assumed to use 0 as baseline.
 | 
						|
   *
 | 
						|
   * @param aDiv Outer parent div
 | 
						|
   * @param aHgram The histogram data
 | 
						|
   * @param aOptions Object with render options (@see #render)
 | 
						|
   */
 | 
						|
  renderValues: function Histogram_renderValues(aDiv, aHgram, aOptions) {
 | 
						|
    let text = "";
 | 
						|
    // If the last label is not the longest string, alignment will break a little
 | 
						|
    let labelPadTo = 0;
 | 
						|
    if (aHgram.values.length) {
 | 
						|
      labelPadTo = String(aHgram.values[aHgram.values.length - 1][0]).length;
 | 
						|
    }
 | 
						|
    let maxBarValue = aOptions.exponential
 | 
						|
      ? this.getLogValue(aHgram.max)
 | 
						|
      : aHgram.max;
 | 
						|
 | 
						|
    for (let [label, value] of aHgram.values) {
 | 
						|
      label = String(label);
 | 
						|
      let barValue = aOptions.exponential ? this.getLogValue(value) : value;
 | 
						|
 | 
						|
      // Create a text representation: <right-aligned-label> |<bar-of-#><value>  <percentage>
 | 
						|
      text +=
 | 
						|
        EOL +
 | 
						|
        " ".repeat(Math.max(0, labelPadTo - label.length)) +
 | 
						|
        label + // Right-aligned label
 | 
						|
        " |" +
 | 
						|
        "#".repeat(Math.round((MAX_BAR_CHARS * barValue) / maxBarValue)) + // Bar
 | 
						|
        "  " +
 | 
						|
        value + // Value
 | 
						|
        "  " +
 | 
						|
        Math.round((100 * value) / aHgram.sample_count) +
 | 
						|
        "%"; // Percentage
 | 
						|
 | 
						|
      // Construct the HTML labels + bars
 | 
						|
      let belowEm =
 | 
						|
        Math.round(MAX_BAR_HEIGHT * (barValue / maxBarValue) * 10) / 10;
 | 
						|
      let aboveEm = MAX_BAR_HEIGHT - belowEm;
 | 
						|
 | 
						|
      let barDiv = document.createElement("div");
 | 
						|
      barDiv.className = "bar";
 | 
						|
      barDiv.style.paddingTop = aboveEm + "em";
 | 
						|
 | 
						|
      // Add value label or an nbsp if no value
 | 
						|
      barDiv.appendChild(document.createTextNode(value ? value : "\u00A0"));
 | 
						|
 | 
						|
      // Create the blue bar
 | 
						|
      let bar = document.createElement("div");
 | 
						|
      bar.className = "bar-inner";
 | 
						|
      bar.style.height = belowEm + "em";
 | 
						|
      barDiv.appendChild(bar);
 | 
						|
 | 
						|
      // Add a special class to move the text down to prevent text overlap
 | 
						|
      if (label.length > 3) {
 | 
						|
        bar.classList.add("long-label");
 | 
						|
      }
 | 
						|
      // Add bucket label
 | 
						|
      barDiv.appendChild(document.createTextNode(label));
 | 
						|
 | 
						|
      aDiv.appendChild(barDiv);
 | 
						|
    }
 | 
						|
 | 
						|
    return text.substr(EOL.length); // Trim the EOL before the first line
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
var Search = {
 | 
						|
  HASH_SEARCH: "search=",
 | 
						|
 | 
						|
  // A list of ids of sections that do not support search.
 | 
						|
  blacklist: ["late-writes-section", "raw-payload-section"],
 | 
						|
 | 
						|
  // Pass if: all non-empty array items match (case-sensitive)
 | 
						|
  isPassText(subject, filter) {
 | 
						|
    for (let item of filter) {
 | 
						|
      if (item.length && !subject.includes(item)) {
 | 
						|
        return false; // mismatch and not a spurious space
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return true;
 | 
						|
  },
 | 
						|
 | 
						|
  isPassRegex(subject, filter) {
 | 
						|
    return filter.test(subject);
 | 
						|
  },
 | 
						|
 | 
						|
  chooseFilter(filterText) {
 | 
						|
    let filter = filterText.toString();
 | 
						|
    // Setup normalized filter string (trimmed, lower cased and split on spaces if not RegEx)
 | 
						|
    let isPassFunc; // filter function, set once, then applied to all elements
 | 
						|
    filter = filter.trim();
 | 
						|
    if (filter[0] != "/") {
 | 
						|
      // Plain text: case insensitive, AND if multi-string
 | 
						|
      isPassFunc = this.isPassText;
 | 
						|
      filter = filter.toLowerCase().split(" ");
 | 
						|
    } else {
 | 
						|
      isPassFunc = this.isPassRegex;
 | 
						|
      var r = filter.match(/^\/(.*)\/(i?)$/);
 | 
						|
      try {
 | 
						|
        filter = RegExp(r[1], r[2]);
 | 
						|
      } catch (e) {
 | 
						|
        // Incomplete or bad RegExp - always no match
 | 
						|
        isPassFunc = function () {
 | 
						|
          return false;
 | 
						|
        };
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return [isPassFunc, filter];
 | 
						|
  },
 | 
						|
 | 
						|
  filterTextRows(table, filterText) {
 | 
						|
    let [isPassFunc, filter] = this.chooseFilter(filterText);
 | 
						|
    let allElementHidden = true;
 | 
						|
 | 
						|
    let needLowerCase = isPassFunc === this.isPassText;
 | 
						|
    let elements = table.rows;
 | 
						|
    for (let element of elements) {
 | 
						|
      if (element.firstChild.nodeName == "th") {
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
      for (let cell of element.children) {
 | 
						|
        let subject = needLowerCase
 | 
						|
          ? cell.textContent.toLowerCase()
 | 
						|
          : cell.textContent;
 | 
						|
        element.hidden = !isPassFunc(subject, filter);
 | 
						|
        if (!element.hidden) {
 | 
						|
          if (allElementHidden) {
 | 
						|
            allElementHidden = false;
 | 
						|
          }
 | 
						|
          // Don't need to check the rest of this row.
 | 
						|
          break;
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
    // Unhide the first row:
 | 
						|
    if (!allElementHidden) {
 | 
						|
      table.rows[0].hidden = false;
 | 
						|
    }
 | 
						|
    return allElementHidden;
 | 
						|
  },
 | 
						|
 | 
						|
  filterElements(elements, filterText) {
 | 
						|
    let [isPassFunc, filter] = this.chooseFilter(filterText);
 | 
						|
    let allElementHidden = true;
 | 
						|
 | 
						|
    let needLowerCase = isPassFunc === this.isPassText;
 | 
						|
    for (let element of elements) {
 | 
						|
      let subject = needLowerCase ? element.id.toLowerCase() : element.id;
 | 
						|
      element.hidden = !isPassFunc(subject, filter);
 | 
						|
      if (allElementHidden && !element.hidden) {
 | 
						|
        allElementHidden = false;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return allElementHidden;
 | 
						|
  },
 | 
						|
 | 
						|
  filterKeyedElements(keyedElements, filterText) {
 | 
						|
    let [isPassFunc, filter] = this.chooseFilter(filterText);
 | 
						|
    let allElementsHidden = true;
 | 
						|
 | 
						|
    let needLowerCase = isPassFunc === this.isPassText;
 | 
						|
    keyedElements.forEach(keyedElement => {
 | 
						|
      let subject = needLowerCase
 | 
						|
        ? keyedElement.key.id.toLowerCase()
 | 
						|
        : keyedElement.key.id;
 | 
						|
      if (!isPassFunc(subject, filter)) {
 | 
						|
        // If the keyedHistogram's name is not matched
 | 
						|
        let allKeyedElementsHidden = true;
 | 
						|
        for (let element of keyedElement.datas) {
 | 
						|
          let subject = needLowerCase ? element.id.toLowerCase() : element.id;
 | 
						|
          let match = isPassFunc(subject, filter);
 | 
						|
          element.hidden = !match;
 | 
						|
          if (match) {
 | 
						|
            allKeyedElementsHidden = false;
 | 
						|
          }
 | 
						|
        }
 | 
						|
        if (allElementsHidden && !allKeyedElementsHidden) {
 | 
						|
          allElementsHidden = false;
 | 
						|
        }
 | 
						|
        keyedElement.key.hidden = allKeyedElementsHidden;
 | 
						|
      } else {
 | 
						|
        // If the keyedHistogram's name is matched
 | 
						|
        allElementsHidden = false;
 | 
						|
        keyedElement.key.hidden = false;
 | 
						|
        for (let element of keyedElement.datas) {
 | 
						|
          element.hidden = false;
 | 
						|
        }
 | 
						|
      }
 | 
						|
    });
 | 
						|
    return allElementsHidden;
 | 
						|
  },
 | 
						|
 | 
						|
  searchHandler(e) {
 | 
						|
    if (this.idleTimeout) {
 | 
						|
      clearTimeout(this.idleTimeout);
 | 
						|
    }
 | 
						|
    this.idleTimeout = setTimeout(
 | 
						|
      () => Search.search(e.target.value),
 | 
						|
      FILTER_IDLE_TIMEOUT
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  search(text, sectionParam = null) {
 | 
						|
    let section = sectionParam;
 | 
						|
    if (!section) {
 | 
						|
      let sectionId = document
 | 
						|
        .querySelector(".category.selected")
 | 
						|
        .getAttribute("value");
 | 
						|
      section = document.getElementById(sectionId);
 | 
						|
    }
 | 
						|
    if (Search.blacklist.includes(section.id)) {
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    let noSearchResults = true;
 | 
						|
    // In the home section, we search all other sections:
 | 
						|
    if (section.id === "home-section") {
 | 
						|
      return this.homeSearch(text);
 | 
						|
    }
 | 
						|
 | 
						|
    if (section.id === "histograms-section") {
 | 
						|
      let histograms = section.getElementsByClassName("histogram");
 | 
						|
      noSearchResults = this.filterElements(histograms, text);
 | 
						|
    } else if (section.id === "keyed-histograms-section") {
 | 
						|
      let keyedElements = [];
 | 
						|
      let keyedHistograms = section.getElementsByClassName("keyed-histogram");
 | 
						|
      for (let key of keyedHistograms) {
 | 
						|
        let datas = key.getElementsByClassName("histogram");
 | 
						|
        keyedElements.push({ key, datas });
 | 
						|
      }
 | 
						|
      noSearchResults = this.filterKeyedElements(keyedElements, text);
 | 
						|
    } else if (section.id === "keyed-scalars-section") {
 | 
						|
      let keyedElements = [];
 | 
						|
      let keyedScalars = section.getElementsByClassName("keyed-scalar");
 | 
						|
      for (let key of keyedScalars) {
 | 
						|
        let datas = key.querySelector("table").rows;
 | 
						|
        keyedElements.push({ key, datas });
 | 
						|
      }
 | 
						|
      noSearchResults = this.filterKeyedElements(keyedElements, text);
 | 
						|
    } else if (section.matches(".text-search")) {
 | 
						|
      let tables = section.querySelectorAll("table");
 | 
						|
      for (let table of tables) {
 | 
						|
        // If we unhide anything, flip noSearchResults to
 | 
						|
        // false so we don't show the "no results" bits.
 | 
						|
        if (!this.filterTextRows(table, text)) {
 | 
						|
          noSearchResults = false;
 | 
						|
        }
 | 
						|
      }
 | 
						|
    } else if (section.querySelector(".sub-section")) {
 | 
						|
      let keyedSubSections = [];
 | 
						|
      let subsections = section.querySelectorAll(".sub-section");
 | 
						|
      for (let section of subsections) {
 | 
						|
        let datas = section.querySelector("table").rows;
 | 
						|
        keyedSubSections.push({ key: section, datas });
 | 
						|
      }
 | 
						|
      noSearchResults = this.filterKeyedElements(keyedSubSections, text);
 | 
						|
    } else {
 | 
						|
      let tables = section.querySelectorAll("table");
 | 
						|
      for (let table of tables) {
 | 
						|
        noSearchResults = this.filterElements(table.rows, text);
 | 
						|
        if (table.caption) {
 | 
						|
          table.caption.hidden = noSearchResults;
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    changeUrlSearch(text);
 | 
						|
 | 
						|
    if (!sectionParam) {
 | 
						|
      // If we are not searching in all section.
 | 
						|
      this.updateNoResults(text, noSearchResults);
 | 
						|
    }
 | 
						|
    return noSearchResults;
 | 
						|
  },
 | 
						|
 | 
						|
  updateNoResults(text, noSearchResults) {
 | 
						|
    document
 | 
						|
      .getElementById("no-search-results")
 | 
						|
      .classList.toggle("hidden", !noSearchResults);
 | 
						|
    if (noSearchResults) {
 | 
						|
      let section = document.querySelector(".category.selected > span");
 | 
						|
      let searchResultsText = document.getElementById("no-search-results-text");
 | 
						|
      if (section.parentElement.id === "category-home") {
 | 
						|
        document.l10n.setAttributes(
 | 
						|
          searchResultsText,
 | 
						|
          "about-telemetry-no-search-results-all",
 | 
						|
          { searchTerms: text }
 | 
						|
        );
 | 
						|
      } else {
 | 
						|
        let sectionName = section.textContent.trim();
 | 
						|
        text === ""
 | 
						|
          ? document.l10n.setAttributes(
 | 
						|
              searchResultsText,
 | 
						|
              "about-telemetry-no-data-to-display",
 | 
						|
              { sectionName }
 | 
						|
            )
 | 
						|
          : document.l10n.setAttributes(
 | 
						|
              searchResultsText,
 | 
						|
              "about-telemetry-no-search-results",
 | 
						|
              { sectionName, currentSearchText: text }
 | 
						|
            );
 | 
						|
      }
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  resetHome() {
 | 
						|
    document.getElementById("main").classList.remove("search");
 | 
						|
    document.getElementById("no-search-results").classList.add("hidden");
 | 
						|
    adjustHeaderState();
 | 
						|
    Array.from(document.querySelectorAll("section")).forEach(section => {
 | 
						|
      section.classList.toggle("active", section.id == "home-section");
 | 
						|
    });
 | 
						|
  },
 | 
						|
 | 
						|
  homeSearch(text) {
 | 
						|
    changeUrlSearch(text);
 | 
						|
    removeSearchSectionTitles();
 | 
						|
    if (text === "") {
 | 
						|
      this.resetHome();
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    document.getElementById("main").classList.add("search");
 | 
						|
    adjustHeaderState(text);
 | 
						|
    let noSearchResults = true;
 | 
						|
    Array.from(document.querySelectorAll("section")).forEach(section => {
 | 
						|
      if (section.id == "home-section" || section.id == "raw-payload-section") {
 | 
						|
        section.classList.remove("active");
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      section.classList.add("active");
 | 
						|
      let sectionHidden = this.search(text, section);
 | 
						|
      if (!sectionHidden) {
 | 
						|
        let sectionTitle = document.querySelector(
 | 
						|
          `.category[value="${section.id}"] .category-name`
 | 
						|
        ).textContent;
 | 
						|
        let sectionDataDiv = document.querySelector(
 | 
						|
          `#${section.id}.has-data.active .data`
 | 
						|
        );
 | 
						|
        let titleDiv = document.createElement("h1");
 | 
						|
        titleDiv.classList.add("data", "search-section-title");
 | 
						|
        titleDiv.textContent = sectionTitle;
 | 
						|
        section.insertBefore(titleDiv, sectionDataDiv);
 | 
						|
        noSearchResults = false;
 | 
						|
      } else {
 | 
						|
        // Hide all subsections if the section is hidden
 | 
						|
        let subsections = section.querySelectorAll(".sub-section");
 | 
						|
        for (let subsection of subsections) {
 | 
						|
          subsection.hidden = true;
 | 
						|
        }
 | 
						|
      }
 | 
						|
    });
 | 
						|
    this.updateNoResults(text, noSearchResults);
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
/*
 | 
						|
 * Helper function to render JS objects with white space between top level elements
 | 
						|
 * so that they look better in the browser
 | 
						|
 * @param   aObject JavaScript object or array to render
 | 
						|
 * @return  String
 | 
						|
 */
 | 
						|
function RenderObject(aObject) {
 | 
						|
  let output = "";
 | 
						|
  if (Array.isArray(aObject)) {
 | 
						|
    if (!aObject.length) {
 | 
						|
      return "[]";
 | 
						|
    }
 | 
						|
    output = "[" + JSON.stringify(aObject[0]);
 | 
						|
    for (let i = 1; i < aObject.length; i++) {
 | 
						|
      output += ", " + JSON.stringify(aObject[i]);
 | 
						|
    }
 | 
						|
    return output + "]";
 | 
						|
  }
 | 
						|
  let keys = Object.keys(aObject);
 | 
						|
  if (!keys.length) {
 | 
						|
    return "{}";
 | 
						|
  }
 | 
						|
  output = '{"' + keys[0] + '":\u00A0' + JSON.stringify(aObject[keys[0]]);
 | 
						|
  for (let i = 1; i < keys.length; i++) {
 | 
						|
    output += ', "' + keys[i] + '":\u00A0' + JSON.stringify(aObject[keys[i]]);
 | 
						|
  }
 | 
						|
  return output + "}";
 | 
						|
}
 | 
						|
 | 
						|
var GenericSubsection = {
 | 
						|
  addSubSectionToSidebar(id, title) {
 | 
						|
    let category = document.querySelector("#categories > [value=" + id + "]");
 | 
						|
    category.classList.add("has-subsection");
 | 
						|
    let subCategory = document.createElement("div");
 | 
						|
    subCategory.classList.add("category-subsection");
 | 
						|
    subCategory.setAttribute("value", id + "-" + title);
 | 
						|
    subCategory.addEventListener("click", ev => {
 | 
						|
      let section = ev.target;
 | 
						|
      showSubSection(section);
 | 
						|
    });
 | 
						|
    subCategory.appendChild(document.createTextNode(title));
 | 
						|
    category.appendChild(subCategory);
 | 
						|
  },
 | 
						|
 | 
						|
  render(data, dataDiv, sectionID) {
 | 
						|
    for (let [title, sectionData] of data) {
 | 
						|
      let hasData = sectionData.size > 0;
 | 
						|
      let s = this.renderSubsectionHeader(title, hasData, sectionID);
 | 
						|
      s.appendChild(this.renderSubsectionData(title, sectionData));
 | 
						|
      dataDiv.appendChild(s);
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  renderSubsectionHeader(title, hasData, sectionID) {
 | 
						|
    this.addSubSectionToSidebar(sectionID, title);
 | 
						|
    let section = document.createElement("div");
 | 
						|
    section.setAttribute("id", sectionID + "-" + title);
 | 
						|
    section.classList.add("sub-section");
 | 
						|
    if (hasData) {
 | 
						|
      section.classList.add("has-subdata");
 | 
						|
    }
 | 
						|
    return section;
 | 
						|
  },
 | 
						|
 | 
						|
  renderSubsectionData(title, data) {
 | 
						|
    // Create data container
 | 
						|
    let dataDiv = document.createElement("div");
 | 
						|
    dataDiv.setAttribute("class", "subsection-data subdata");
 | 
						|
    // Instanciate the data
 | 
						|
    let table = GenericTable.render(data);
 | 
						|
    let caption = document.createElement("caption");
 | 
						|
    caption.textContent = title;
 | 
						|
    table.appendChild(caption);
 | 
						|
    dataDiv.appendChild(table);
 | 
						|
 | 
						|
    return dataDiv;
 | 
						|
  },
 | 
						|
 | 
						|
  deleteAllSubSections() {
 | 
						|
    let subsections = document.querySelectorAll(".category-subsection");
 | 
						|
    subsections.forEach(el => {
 | 
						|
      el.parentElement.removeChild(el);
 | 
						|
    });
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
var GenericTable = {
 | 
						|
  // Returns a table with key and value headers
 | 
						|
  defaultHeadings() {
 | 
						|
    return ["about-telemetry-keys-header", "about-telemetry-values-header"];
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Returns a n-column table.
 | 
						|
   * @param rows An array of arrays, each containing data to render
 | 
						|
   *             for one row.
 | 
						|
   * @param headings The column header strings.
 | 
						|
   */
 | 
						|
  render(rows, headings = this.defaultHeadings()) {
 | 
						|
    let table = document.createElement("table");
 | 
						|
    this.renderHeader(table, headings);
 | 
						|
    this.renderBody(table, rows);
 | 
						|
    return table;
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Create the table header.
 | 
						|
   * Tabs & newlines added to cells to make it easier to copy-paste.
 | 
						|
   *
 | 
						|
   * @param table Table element
 | 
						|
   * @param headings Array of column header strings.
 | 
						|
   */
 | 
						|
  renderHeader(table, headings) {
 | 
						|
    let headerRow = document.createElement("tr");
 | 
						|
    table.appendChild(headerRow);
 | 
						|
 | 
						|
    for (let i = 0; i < headings.length; ++i) {
 | 
						|
      let column = document.createElement("th");
 | 
						|
      document.l10n.setAttributes(column, headings[i]);
 | 
						|
      headerRow.appendChild(column);
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Create the table body
 | 
						|
   * Tabs & newlines added to cells to make it easier to copy-paste.
 | 
						|
   *
 | 
						|
   * @param table Table element
 | 
						|
   * @param rows An array of arrays, each containing data to render
 | 
						|
   *             for one row.
 | 
						|
   */
 | 
						|
  renderBody(table, rows) {
 | 
						|
    for (let row of rows) {
 | 
						|
      row = row.map(value => {
 | 
						|
        // use .valueOf() to unbox Number, String, etc. objects
 | 
						|
        if (
 | 
						|
          value &&
 | 
						|
          typeof value == "object" &&
 | 
						|
          typeof value.valueOf() == "object"
 | 
						|
        ) {
 | 
						|
          return RenderObject(value);
 | 
						|
        }
 | 
						|
        return value;
 | 
						|
      });
 | 
						|
 | 
						|
      let newRow = document.createElement("tr");
 | 
						|
      newRow.id = row[0];
 | 
						|
      table.appendChild(newRow);
 | 
						|
 | 
						|
      for (let i = 0; i < row.length; ++i) {
 | 
						|
        let suffix = i == row.length - 1 ? "\n" : "\t";
 | 
						|
        let field = document.createElement("td");
 | 
						|
        field.appendChild(document.createTextNode(row[i] + suffix));
 | 
						|
        newRow.appendChild(field);
 | 
						|
      }
 | 
						|
    }
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
var KeyedHistogram = {
 | 
						|
  render(parent, id, keyedHistogram) {
 | 
						|
    let outerDiv = document.createElement("div");
 | 
						|
    outerDiv.className = "keyed-histogram";
 | 
						|
    outerDiv.id = id;
 | 
						|
 | 
						|
    let divTitle = document.createElement("div");
 | 
						|
    divTitle.classList.add("keyed-title");
 | 
						|
    divTitle.appendChild(document.createTextNode(id));
 | 
						|
    outerDiv.appendChild(divTitle);
 | 
						|
 | 
						|
    for (let [name, hgram] of Object.entries(keyedHistogram)) {
 | 
						|
      Histogram.render(outerDiv, name, hgram);
 | 
						|
    }
 | 
						|
 | 
						|
    parent.appendChild(outerDiv);
 | 
						|
    return outerDiv;
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
var AddonDetails = {
 | 
						|
  /**
 | 
						|
   * Render the addon details section as a series of headers followed by key/value tables
 | 
						|
   * @param aPing A ping object to render the data from.
 | 
						|
   */
 | 
						|
  render(aPing) {
 | 
						|
    let addonSection = document.getElementById("addon-details");
 | 
						|
    removeAllChildNodes(addonSection);
 | 
						|
    let addonDetails = aPing.payload.addonDetails;
 | 
						|
    const hasData = addonDetails && !!Object.keys(addonDetails).length;
 | 
						|
    setHasData("addon-details-section", hasData);
 | 
						|
    if (!hasData) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    for (let provider in addonDetails) {
 | 
						|
      let providerSection = document.createElement("caption");
 | 
						|
      document.l10n.setAttributes(
 | 
						|
        providerSection,
 | 
						|
        "about-telemetry-addon-provider",
 | 
						|
        { addonProvider: provider }
 | 
						|
      );
 | 
						|
      let headingStrings = [
 | 
						|
        "about-telemetry-addon-table-id",
 | 
						|
        "about-telemetry-addon-table-details",
 | 
						|
      ];
 | 
						|
      let table = GenericTable.render(
 | 
						|
        explodeObject(addonDetails[provider]),
 | 
						|
        headingStrings
 | 
						|
      );
 | 
						|
      table.appendChild(providerSection);
 | 
						|
      addonSection.appendChild(table);
 | 
						|
    }
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
class Section {
 | 
						|
  static renderContent(data, process, div, section) {
 | 
						|
    if (data && Object.keys(data).length) {
 | 
						|
      let s = GenericSubsection.renderSubsectionHeader(process, true, section);
 | 
						|
      let heading = document.createElement("h2");
 | 
						|
      document.l10n.setAttributes(heading, "about-telemetry-process", {
 | 
						|
        process,
 | 
						|
      });
 | 
						|
      s.appendChild(heading);
 | 
						|
 | 
						|
      this.renderData(data, s);
 | 
						|
 | 
						|
      div.appendChild(s);
 | 
						|
      let separator = document.createElement("div");
 | 
						|
      separator.classList.add("clearfix");
 | 
						|
      div.appendChild(separator);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Make parent process the first one, content process the second
 | 
						|
   * then sort processes alphabetically
 | 
						|
   */
 | 
						|
  static processesComparator(a, b) {
 | 
						|
    if (a === "parent" || (a === "content" && b !== "parent")) {
 | 
						|
      return -1;
 | 
						|
    } else if (b === "parent" || b === "content") {
 | 
						|
      return 1;
 | 
						|
    } else if (a < b) {
 | 
						|
      return -1;
 | 
						|
    } else if (a > b) {
 | 
						|
      return 1;
 | 
						|
    }
 | 
						|
    return 0;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Render sections
 | 
						|
   */
 | 
						|
  static renderSection(divName, section, aPayload) {
 | 
						|
    let div = document.getElementById(divName);
 | 
						|
    removeAllChildNodes(div);
 | 
						|
 | 
						|
    let data = {};
 | 
						|
    let hasData = false;
 | 
						|
    let selectedStore = getSelectedStore();
 | 
						|
 | 
						|
    let payload = aPayload.stores;
 | 
						|
 | 
						|
    let isCurrentPayload = !!payload;
 | 
						|
 | 
						|
    // Sort processes
 | 
						|
    let sortedProcesses = isCurrentPayload
 | 
						|
      ? Object.keys(payload[selectedStore]).sort(this.processesComparator)
 | 
						|
      : Object.keys(aPayload.processes).sort(this.processesComparator);
 | 
						|
 | 
						|
    // Render content by process
 | 
						|
    for (const process of sortedProcesses) {
 | 
						|
      data = isCurrentPayload
 | 
						|
        ? this.dataFiltering(payload, selectedStore, process)
 | 
						|
        : this.archivePingDataFiltering(aPayload, process);
 | 
						|
      hasData = hasData || !ObjectUtils.isEmpty(data);
 | 
						|
      this.renderContent(data, process, div, section, this.renderData);
 | 
						|
    }
 | 
						|
    setHasData(section, hasData);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class Scalars extends Section {
 | 
						|
  /**
 | 
						|
   * Return data from the current ping
 | 
						|
   */
 | 
						|
  static dataFiltering(payload, selectedStore, process) {
 | 
						|
    return payload[selectedStore][process].scalars;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Return data from an archived ping
 | 
						|
   */
 | 
						|
  static archivePingDataFiltering(payload, process) {
 | 
						|
    return payload.processes[process].scalars;
 | 
						|
  }
 | 
						|
 | 
						|
  static renderData(data, div) {
 | 
						|
    const scalarsHeadings = [
 | 
						|
      "about-telemetry-names-header",
 | 
						|
      "about-telemetry-values-header",
 | 
						|
    ];
 | 
						|
    let scalarsTable = GenericTable.render(
 | 
						|
      explodeObject(data),
 | 
						|
      scalarsHeadings
 | 
						|
    );
 | 
						|
    div.appendChild(scalarsTable);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Render the scalar data - if present - from the payload in a simple key-value table.
 | 
						|
   * @param aPayload A payload object to render the data from.
 | 
						|
   */
 | 
						|
  static render(aPayload) {
 | 
						|
    const divName = "scalars";
 | 
						|
    const section = "scalars-section";
 | 
						|
    this.renderSection(divName, section, aPayload);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class KeyedScalars extends Section {
 | 
						|
  /**
 | 
						|
   * Return data from the current ping
 | 
						|
   */
 | 
						|
  static dataFiltering(payload, selectedStore, process) {
 | 
						|
    return payload[selectedStore][process].keyedScalars;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Return data from an archived ping
 | 
						|
   */
 | 
						|
  static archivePingDataFiltering(payload, process) {
 | 
						|
    return payload.processes[process].keyedScalars;
 | 
						|
  }
 | 
						|
 | 
						|
  static renderData(data, div) {
 | 
						|
    const scalarsHeadings = [
 | 
						|
      "about-telemetry-names-header",
 | 
						|
      "about-telemetry-values-header",
 | 
						|
    ];
 | 
						|
    for (let scalarId in data) {
 | 
						|
      // Add the name of the scalar.
 | 
						|
      let container = document.createElement("div");
 | 
						|
      container.classList.add("keyed-scalar");
 | 
						|
      container.id = scalarId;
 | 
						|
      let scalarNameSection = document.createElement("p");
 | 
						|
      scalarNameSection.classList.add("keyed-title");
 | 
						|
      scalarNameSection.appendChild(document.createTextNode(scalarId));
 | 
						|
      container.appendChild(scalarNameSection);
 | 
						|
      // Populate the section with the key-value pairs from the scalar.
 | 
						|
      const table = GenericTable.render(
 | 
						|
        explodeObject(data[scalarId]),
 | 
						|
        scalarsHeadings
 | 
						|
      );
 | 
						|
      container.appendChild(table);
 | 
						|
      div.appendChild(container);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Render the keyed scalar data - if present - from the payload in a simple key-value table.
 | 
						|
   * @param aPayload A payload object to render the data from.
 | 
						|
   */
 | 
						|
  static render(aPayload) {
 | 
						|
    const divName = "keyed-scalars";
 | 
						|
    const section = "keyed-scalars-section";
 | 
						|
    this.renderSection(divName, section, aPayload);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
var Events = {
 | 
						|
  /**
 | 
						|
   * Render the event data - if present - from the payload in a simple table.
 | 
						|
   * @param aPayload A payload object to render the data from.
 | 
						|
   */
 | 
						|
  render(aPayload) {
 | 
						|
    let eventsDiv = document.getElementById("events");
 | 
						|
    removeAllChildNodes(eventsDiv);
 | 
						|
    const headings = [
 | 
						|
      "about-telemetry-time-stamp-header",
 | 
						|
      "about-telemetry-category-header",
 | 
						|
      "about-telemetry-method-header",
 | 
						|
      "about-telemetry-object-header",
 | 
						|
      "about-telemetry-values-header",
 | 
						|
      "about-telemetry-extra-header",
 | 
						|
    ];
 | 
						|
    let payload = aPayload.processes;
 | 
						|
    let hasData = false;
 | 
						|
    if (payload) {
 | 
						|
      for (const process of Object.keys(aPayload.processes)) {
 | 
						|
        let data = aPayload.processes[process].events;
 | 
						|
        if (data && Object.keys(data).length) {
 | 
						|
          hasData = true;
 | 
						|
          let s = GenericSubsection.renderSubsectionHeader(
 | 
						|
            process,
 | 
						|
            true,
 | 
						|
            "events-section"
 | 
						|
          );
 | 
						|
          let heading = document.createElement("h2");
 | 
						|
          heading.textContent = process;
 | 
						|
          s.appendChild(heading);
 | 
						|
          const table = GenericTable.render(data, headings);
 | 
						|
          s.appendChild(table);
 | 
						|
          eventsDiv.appendChild(s);
 | 
						|
          let separator = document.createElement("div");
 | 
						|
          separator.classList.add("clearfix");
 | 
						|
          eventsDiv.appendChild(separator);
 | 
						|
        }
 | 
						|
      }
 | 
						|
    } else {
 | 
						|
      // handle archived ping
 | 
						|
      for (const process of Object.keys(aPayload.events)) {
 | 
						|
        let data = process;
 | 
						|
        if (data && Object.keys(data).length) {
 | 
						|
          hasData = true;
 | 
						|
          let s = GenericSubsection.renderSubsectionHeader(
 | 
						|
            process,
 | 
						|
            true,
 | 
						|
            "events-section"
 | 
						|
          );
 | 
						|
          let heading = document.createElement("h2");
 | 
						|
          heading.textContent = process;
 | 
						|
          s.appendChild(heading);
 | 
						|
          const table = GenericTable.render(data, headings);
 | 
						|
          eventsDiv.appendChild(table);
 | 
						|
          let separator = document.createElement("div");
 | 
						|
          separator.classList.add("clearfix");
 | 
						|
          eventsDiv.appendChild(separator);
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
    setHasData("events-section", hasData);
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * Helper function for showing either the toggle element or "No data collected" message for a section
 | 
						|
 *
 | 
						|
 * @param aSectionID ID of the section element that needs to be changed
 | 
						|
 * @param aHasData true (default) indicates that toggle should be displayed
 | 
						|
 */
 | 
						|
function setHasData(aSectionID, aHasData) {
 | 
						|
  let sectionElement = document.getElementById(aSectionID);
 | 
						|
  sectionElement.classList[aHasData ? "add" : "remove"]("has-data");
 | 
						|
 | 
						|
  // Display or Hide the section in the sidebar
 | 
						|
  let sectionCategory = document.querySelector(
 | 
						|
    ".category[value=" + aSectionID + "]"
 | 
						|
  );
 | 
						|
  sectionCategory.classList[aHasData ? "add" : "remove"]("has-data");
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Sets l10n attributes based on the Telemetry Server Owner pref.
 | 
						|
 */
 | 
						|
function setupServerOwnerBranding() {
 | 
						|
  let serverOwner = Preferences.get(PREF_TELEMETRY_SERVER_OWNER, "Mozilla");
 | 
						|
  const elements = [
 | 
						|
    [document.getElementById("page-subtitle"), "about-telemetry-page-subtitle"],
 | 
						|
  ];
 | 
						|
  for (const [elt, l10nName] of elements) {
 | 
						|
    document.l10n.setAttributes(elt, l10nName, {
 | 
						|
      telemetryServerOwner: serverOwner,
 | 
						|
    });
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Display the store selector if we are on one
 | 
						|
 * of the whitelisted sections
 | 
						|
 */
 | 
						|
function displayStoresSelector(selectedSection) {
 | 
						|
  let whitelist = [
 | 
						|
    "scalars-section",
 | 
						|
    "keyed-scalars-section",
 | 
						|
    "histograms-section",
 | 
						|
    "keyed-histograms-section",
 | 
						|
  ];
 | 
						|
  let stores = document.getElementById("stores");
 | 
						|
  stores.hidden = !whitelist.includes(selectedSection);
 | 
						|
  let storesLabel = document.getElementById("storesLabel");
 | 
						|
  storesLabel.hidden = !whitelist.includes(selectedSection);
 | 
						|
}
 | 
						|
 | 
						|
function refreshSearch() {
 | 
						|
  removeSearchSectionTitles();
 | 
						|
  let selectedSection = document
 | 
						|
    .querySelector(".category.selected")
 | 
						|
    .getAttribute("value");
 | 
						|
  let search = document.getElementById("search");
 | 
						|
  if (!Search.blacklist.includes(selectedSection)) {
 | 
						|
    Search.search(search.value);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function adjustSearchState() {
 | 
						|
  removeSearchSectionTitles();
 | 
						|
  let selectedSection = document
 | 
						|
    .querySelector(".category.selected")
 | 
						|
    .getAttribute("value");
 | 
						|
  let search = document.getElementById("search");
 | 
						|
  search.value = "";
 | 
						|
  search.hidden = Search.blacklist.includes(selectedSection);
 | 
						|
  document.getElementById("no-search-results").classList.add("hidden");
 | 
						|
  Search.search(""); // reinitialize search state.
 | 
						|
}
 | 
						|
 | 
						|
function removeSearchSectionTitles() {
 | 
						|
  for (let sectionTitleDiv of Array.from(
 | 
						|
    document.getElementsByClassName("search-section-title")
 | 
						|
  )) {
 | 
						|
    sectionTitleDiv.remove();
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function adjustSection() {
 | 
						|
  let selectedCategory = document.querySelector(".category.selected");
 | 
						|
  if (!selectedCategory.classList.contains("has-data")) {
 | 
						|
    PingPicker._showStructuredPingData();
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function adjustHeaderState(title = null) {
 | 
						|
  let selected = document.querySelector(".category.selected .category-name");
 | 
						|
  let selectedTitle = selected.textContent.trim();
 | 
						|
  let sectionTitle = document.getElementById("sectionTitle");
 | 
						|
  if (title !== null) {
 | 
						|
    document.l10n.setAttributes(
 | 
						|
      sectionTitle,
 | 
						|
      "about-telemetry-results-for-search",
 | 
						|
      { searchTerms: title }
 | 
						|
    );
 | 
						|
  } else {
 | 
						|
    sectionTitle.textContent = selectedTitle;
 | 
						|
  }
 | 
						|
  let search = document.getElementById("search");
 | 
						|
  if (selected.parentElement.id === "category-home") {
 | 
						|
    document.l10n.setAttributes(
 | 
						|
      search,
 | 
						|
      "about-telemetry-filter-all-placeholder"
 | 
						|
    );
 | 
						|
  } else {
 | 
						|
    document.l10n.setAttributes(search, "about-telemetry-filter-placeholder", {
 | 
						|
      selectedTitle,
 | 
						|
    });
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Change the url according to the current section displayed
 | 
						|
 * e.g about:telemetry#general-data
 | 
						|
 */
 | 
						|
function changeUrlPath(selectedSection, subSection) {
 | 
						|
  if (subSection) {
 | 
						|
    let hash = window.location.hash.split("_")[0] + "_" + selectedSection;
 | 
						|
    window.location.hash = hash;
 | 
						|
  } else {
 | 
						|
    window.location.hash = selectedSection.replace("-section", "-tab");
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Change the url according to the current search text
 | 
						|
 */
 | 
						|
function changeUrlSearch(searchText) {
 | 
						|
  let currentHash = window.location.hash;
 | 
						|
  let hashWithoutSearch = currentHash.split(Search.HASH_SEARCH)[0];
 | 
						|
  let hash = "";
 | 
						|
 | 
						|
  if (!currentHash && !searchText) {
 | 
						|
    return;
 | 
						|
  }
 | 
						|
  if (!currentHash.includes(Search.HASH_SEARCH) && hashWithoutSearch) {
 | 
						|
    hashWithoutSearch += "_";
 | 
						|
  }
 | 
						|
  if (searchText) {
 | 
						|
    hash =
 | 
						|
      hashWithoutSearch + Search.HASH_SEARCH + searchText.replace(/ /g, "+");
 | 
						|
  } else if (hashWithoutSearch) {
 | 
						|
    hash = hashWithoutSearch.slice(0, hashWithoutSearch.length - 1);
 | 
						|
  }
 | 
						|
 | 
						|
  window.location.hash = hash;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Change the section displayed
 | 
						|
 */
 | 
						|
function show(selected) {
 | 
						|
  let selectedValue = selected.getAttribute("value");
 | 
						|
  if (selectedValue === "raw-json-viewer") {
 | 
						|
    openJsonInFirefoxJsonViewer(JSON.stringify(gPingData, null, 2));
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  let selected_section = document.getElementById(selectedValue);
 | 
						|
  let subsections = selected_section.querySelectorAll(".sub-section");
 | 
						|
  if (selected.classList.contains("has-subsection")) {
 | 
						|
    for (let subsection of selected.children) {
 | 
						|
      subsection.classList.remove("selected");
 | 
						|
    }
 | 
						|
  }
 | 
						|
  if (subsections) {
 | 
						|
    for (let subsection of subsections) {
 | 
						|
      subsection.hidden = false;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  let current_button = document.querySelector(".category.selected");
 | 
						|
  if (current_button == selected) {
 | 
						|
    return;
 | 
						|
  }
 | 
						|
  current_button.classList.remove("selected");
 | 
						|
  selected.classList.add("selected");
 | 
						|
 | 
						|
  document.querySelectorAll("section").forEach(section => {
 | 
						|
    section.classList.remove("active");
 | 
						|
  });
 | 
						|
  selected_section.classList.add("active");
 | 
						|
 | 
						|
  adjustHeaderState();
 | 
						|
  displayStoresSelector(selectedValue);
 | 
						|
  adjustSearchState();
 | 
						|
  changeUrlPath(selectedValue);
 | 
						|
}
 | 
						|
 | 
						|
function showSubSection(selected) {
 | 
						|
  if (!selected) {
 | 
						|
    return;
 | 
						|
  }
 | 
						|
  let current_selection = document.querySelector(
 | 
						|
    ".category-subsection.selected"
 | 
						|
  );
 | 
						|
  if (current_selection) {
 | 
						|
    current_selection.classList.remove("selected");
 | 
						|
  }
 | 
						|
  selected.classList.add("selected");
 | 
						|
 | 
						|
  let section = document.getElementById(selected.getAttribute("value"));
 | 
						|
  section.parentElement.childNodes.forEach(element => {
 | 
						|
    element.hidden = true;
 | 
						|
  });
 | 
						|
  section.hidden = false;
 | 
						|
 | 
						|
  let title =
 | 
						|
    selected.parentElement.querySelector(".category-name").textContent;
 | 
						|
  let subsection = selected.textContent;
 | 
						|
  document.getElementById("sectionTitle").textContent =
 | 
						|
    title + " - " + subsection;
 | 
						|
  changeUrlPath(subsection, true);
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Initializes load/unload, pref change and mouse-click listeners
 | 
						|
 */
 | 
						|
function setupListeners() {
 | 
						|
  Settings.attachObservers();
 | 
						|
  PingPicker.attachObservers();
 | 
						|
  RawPayloadData.attachObservers();
 | 
						|
 | 
						|
  let menu = document.getElementById("categories");
 | 
						|
  menu.addEventListener("click", e => {
 | 
						|
    if (e.target && e.target.parentNode == menu) {
 | 
						|
      show(e.target);
 | 
						|
    }
 | 
						|
  });
 | 
						|
 | 
						|
  let search = document.getElementById("search");
 | 
						|
  search.addEventListener("input", Search.searchHandler);
 | 
						|
 | 
						|
  document
 | 
						|
    .getElementById("late-writes-fetch-symbols")
 | 
						|
    .addEventListener("click", function () {
 | 
						|
      if (!gPingData) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      let lateWrites = gPingData.payload.lateWrites;
 | 
						|
      let req = new SymbolicationRequest(
 | 
						|
        "late-writes",
 | 
						|
        LateWritesSingleton.renderHeader,
 | 
						|
        lateWrites.memoryMap,
 | 
						|
        lateWrites.stacks
 | 
						|
      );
 | 
						|
      req.fetchSymbols();
 | 
						|
    });
 | 
						|
 | 
						|
  document
 | 
						|
    .getElementById("late-writes-hide-symbols")
 | 
						|
    .addEventListener("click", function () {
 | 
						|
      if (!gPingData) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
 | 
						|
      LateWritesSingleton.renderLateWrites(gPingData.payload.lateWrites);
 | 
						|
    });
 | 
						|
}
 | 
						|
 | 
						|
// Restores the sections states
 | 
						|
function urlSectionRestore(hash) {
 | 
						|
  if (hash) {
 | 
						|
    let section = hash.replace("-tab", "-section");
 | 
						|
    let subsection = section.split("_")[1];
 | 
						|
    section = section.split("_")[0];
 | 
						|
    let category = document.querySelector(".category[value=" + section + "]");
 | 
						|
    if (category) {
 | 
						|
      show(category);
 | 
						|
      if (subsection) {
 | 
						|
        let selector =
 | 
						|
          ".category-subsection[value=" + section + "-" + subsection + "]";
 | 
						|
        let subcategory = document.querySelector(selector);
 | 
						|
        showSubSection(subcategory);
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
// Restore sections states and search terms
 | 
						|
function urlStateRestore() {
 | 
						|
  let hash = window.location.hash;
 | 
						|
  let searchQuery = "";
 | 
						|
  if (hash) {
 | 
						|
    hash = hash.slice(1);
 | 
						|
    if (hash.includes(Search.HASH_SEARCH)) {
 | 
						|
      searchQuery = hash.split(Search.HASH_SEARCH)[1].replace(/[+]/g, " ");
 | 
						|
      hash = hash.split(Search.HASH_SEARCH)[0];
 | 
						|
    }
 | 
						|
    urlSectionRestore(hash);
 | 
						|
  }
 | 
						|
  if (searchQuery) {
 | 
						|
    let search = document.getElementById("search");
 | 
						|
    search.value = searchQuery;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function openJsonInFirefoxJsonViewer(json) {
 | 
						|
  json = unescape(encodeURIComponent(json));
 | 
						|
  try {
 | 
						|
    window.open("data:application/json;base64," + btoa(json));
 | 
						|
  } catch (e) {
 | 
						|
    show(document.querySelector(".category[value=raw-payload-section]"));
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function onLoad() {
 | 
						|
  window.removeEventListener("load", onLoad);
 | 
						|
  // Set the text in the page header and elsewhere that needs the server owner.
 | 
						|
  setupServerOwnerBranding();
 | 
						|
 | 
						|
  // Set up event listeners
 | 
						|
  setupListeners();
 | 
						|
 | 
						|
  // Render settings.
 | 
						|
  Settings.render();
 | 
						|
 | 
						|
  adjustHeaderState();
 | 
						|
 | 
						|
  urlStateRestore();
 | 
						|
 | 
						|
  // Update ping data when async Telemetry init is finished.
 | 
						|
  Telemetry.asyncFetchTelemetryData(async () => {
 | 
						|
    await PingPicker.update();
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
var LateWritesSingleton = {
 | 
						|
  renderHeader: function LateWritesSingleton_renderHeader(aIndex) {
 | 
						|
    StackRenderer.renderHeader(
 | 
						|
      "late-writes",
 | 
						|
      "about-telemetry-late-writes-title",
 | 
						|
      { lateWriteCount: aIndex + 1 }
 | 
						|
    );
 | 
						|
  },
 | 
						|
 | 
						|
  renderLateWrites: function LateWritesSingleton_renderLateWrites(lateWrites) {
 | 
						|
    let hasData = !!(
 | 
						|
      lateWrites &&
 | 
						|
      lateWrites.stacks &&
 | 
						|
      lateWrites.stacks.length
 | 
						|
    );
 | 
						|
    setHasData("late-writes-section", hasData);
 | 
						|
    if (!hasData) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    let stacks = lateWrites.stacks;
 | 
						|
    let memoryMap = lateWrites.memoryMap;
 | 
						|
    StackRenderer.renderStacks(
 | 
						|
      "late-writes",
 | 
						|
      stacks,
 | 
						|
      memoryMap,
 | 
						|
      LateWritesSingleton.renderHeader
 | 
						|
    );
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
class HistogramSection extends Section {
 | 
						|
  /**
 | 
						|
   * Return data from the current ping
 | 
						|
   */
 | 
						|
  static dataFiltering(payload, selectedStore, process) {
 | 
						|
    return payload[selectedStore][process].histograms;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Return data from an archived ping
 | 
						|
   */
 | 
						|
  static archivePingDataFiltering(payload, process) {
 | 
						|
    if (process === "parent") {
 | 
						|
      return payload.histograms;
 | 
						|
    }
 | 
						|
    return payload.processes[process].histograms;
 | 
						|
  }
 | 
						|
 | 
						|
  static renderData(data, div) {
 | 
						|
    for (let [hName, hgram] of Object.entries(data)) {
 | 
						|
      Histogram.render(div, hName, hgram, { unpacked: true });
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  static render(aPayload) {
 | 
						|
    const divName = "histograms";
 | 
						|
    const section = "histograms-section";
 | 
						|
    this.renderSection(divName, section, aPayload);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class KeyedHistogramSection extends Section {
 | 
						|
  /**
 | 
						|
   * Return data from the current ping
 | 
						|
   */
 | 
						|
  static dataFiltering(payload, selectedStore, process) {
 | 
						|
    return payload[selectedStore][process].keyedHistograms;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Return data from an archived ping
 | 
						|
   */
 | 
						|
  static archivePingDataFiltering(payload, process) {
 | 
						|
    if (process === "parent") {
 | 
						|
      return payload.keyedHistograms;
 | 
						|
    }
 | 
						|
    return payload.processes[process].keyedHistograms;
 | 
						|
  }
 | 
						|
 | 
						|
  static renderData(data, div) {
 | 
						|
    for (let [id, keyed] of Object.entries(data)) {
 | 
						|
      KeyedHistogram.render(div, id, keyed, { unpacked: true });
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  static render(aPayload) {
 | 
						|
    const divName = "keyed-histograms";
 | 
						|
    const section = "keyed-histograms-section";
 | 
						|
    this.renderSection(divName, section, aPayload);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
var SessionInformation = {
 | 
						|
  render(aPayload) {
 | 
						|
    let infoSection = document.getElementById("session-info");
 | 
						|
    removeAllChildNodes(infoSection);
 | 
						|
 | 
						|
    let hasData = !!Object.keys(aPayload.info).length;
 | 
						|
    setHasData("session-info-section", hasData);
 | 
						|
 | 
						|
    if (hasData) {
 | 
						|
      const table = GenericTable.render(explodeObject(aPayload.info));
 | 
						|
      infoSection.appendChild(table);
 | 
						|
    }
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
var SimpleMeasurements = {
 | 
						|
  render(aPayload) {
 | 
						|
    let simpleSection = document.getElementById("simple-measurements");
 | 
						|
    removeAllChildNodes(simpleSection);
 | 
						|
 | 
						|
    let simpleMeasurements = this.sortStartupMilestones(
 | 
						|
      aPayload.simpleMeasurements
 | 
						|
    );
 | 
						|
    let hasData = !!Object.keys(simpleMeasurements).length;
 | 
						|
    setHasData("simple-measurements-section", hasData);
 | 
						|
 | 
						|
    if (hasData) {
 | 
						|
      const table = GenericTable.render(explodeObject(simpleMeasurements));
 | 
						|
      simpleSection.appendChild(table);
 | 
						|
    }
 | 
						|
  },
 | 
						|
 | 
						|
  /**
 | 
						|
   * Helper function for sorting the startup milestones in the Simple Measurements
 | 
						|
   * section into temporal order.
 | 
						|
   *
 | 
						|
   * @param aSimpleMeasurements Telemetry ping's "Simple Measurements" data
 | 
						|
   * @return Sorted measurements
 | 
						|
   */
 | 
						|
  sortStartupMilestones(aSimpleMeasurements) {
 | 
						|
    const telemetryTimestamps = TelemetryTimestamps.get();
 | 
						|
    let startupEvents = Services.startup.getStartupInfo();
 | 
						|
    delete startupEvents.process;
 | 
						|
 | 
						|
    function keyIsMilestone(k) {
 | 
						|
      return k in startupEvents || k in telemetryTimestamps;
 | 
						|
    }
 | 
						|
 | 
						|
    let sortedKeys = Object.keys(aSimpleMeasurements);
 | 
						|
 | 
						|
    // Sort the measurements, with startup milestones at the front + ordered by time
 | 
						|
    sortedKeys.sort(function keyCompare(keyA, keyB) {
 | 
						|
      let isKeyAMilestone = keyIsMilestone(keyA);
 | 
						|
      let isKeyBMilestone = keyIsMilestone(keyB);
 | 
						|
 | 
						|
      // First order by startup vs non-startup measurement
 | 
						|
      if (isKeyAMilestone && !isKeyBMilestone) {
 | 
						|
        return -1;
 | 
						|
      }
 | 
						|
      if (!isKeyAMilestone && isKeyBMilestone) {
 | 
						|
        return 1;
 | 
						|
      }
 | 
						|
      // Don't change order of non-startup measurements
 | 
						|
      if (!isKeyAMilestone && !isKeyBMilestone) {
 | 
						|
        return 0;
 | 
						|
      }
 | 
						|
 | 
						|
      // If both keys are startup measurements, order them by value
 | 
						|
      return aSimpleMeasurements[keyA] - aSimpleMeasurements[keyB];
 | 
						|
    });
 | 
						|
 | 
						|
    // Insert measurements into a result object in sort-order
 | 
						|
    let result = {};
 | 
						|
    for (let key of sortedKeys) {
 | 
						|
      result[key] = aSimpleMeasurements[key];
 | 
						|
    }
 | 
						|
 | 
						|
    return result;
 | 
						|
  },
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * Render stores options
 | 
						|
 */
 | 
						|
function renderStoreList(payload) {
 | 
						|
  let storeSelect = document.getElementById("stores");
 | 
						|
  let storesLabel = document.getElementById("storesLabel");
 | 
						|
  removeAllChildNodes(storeSelect);
 | 
						|
 | 
						|
  if (!("stores" in payload)) {
 | 
						|
    storeSelect.classList.add("hidden");
 | 
						|
    storesLabel.classList.add("hidden");
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  storeSelect.classList.remove("hidden");
 | 
						|
  storesLabel.classList.remove("hidden");
 | 
						|
  storeSelect.disabled = false;
 | 
						|
 | 
						|
  for (let store of Object.keys(payload.stores)) {
 | 
						|
    let option = document.createElement("option");
 | 
						|
    option.appendChild(document.createTextNode(store));
 | 
						|
    option.setAttribute("value", store);
 | 
						|
    // Select main store by default
 | 
						|
    if (store === "main") {
 | 
						|
      option.selected = true;
 | 
						|
    }
 | 
						|
    storeSelect.appendChild(option);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Return the selected store
 | 
						|
 */
 | 
						|
function getSelectedStore() {
 | 
						|
  let storeSelect = document.getElementById("stores");
 | 
						|
  let storeSelectedOption = storeSelect.selectedOptions.item(0);
 | 
						|
  let selectedStore =
 | 
						|
    storeSelectedOption !== null
 | 
						|
      ? storeSelectedOption.getAttribute("value")
 | 
						|
      : undefined;
 | 
						|
  return selectedStore;
 | 
						|
}
 | 
						|
 | 
						|
function togglePingSections(isMainPing) {
 | 
						|
  // We always show the sections that are "common" to all pings.
 | 
						|
  let commonSections = new Set([
 | 
						|
    "heading",
 | 
						|
    "home-section",
 | 
						|
    "general-data-section",
 | 
						|
    "environment-data-section",
 | 
						|
    "raw-json-viewer",
 | 
						|
  ]);
 | 
						|
 | 
						|
  let elements = document.querySelectorAll(".category");
 | 
						|
  for (let section of elements) {
 | 
						|
    if (commonSections.has(section.getAttribute("value"))) {
 | 
						|
      continue;
 | 
						|
    }
 | 
						|
    // Only show the raw payload for non main ping.
 | 
						|
    if (section.getAttribute("value") == "raw-payload-section") {
 | 
						|
      section.classList.toggle("has-data", !isMainPing);
 | 
						|
    } else {
 | 
						|
      section.classList.toggle("has-data", isMainPing);
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function displayPingData(ping, updatePayloadList = false) {
 | 
						|
  gPingData = ping;
 | 
						|
  try {
 | 
						|
    PingPicker.render();
 | 
						|
    displayRichPingData(ping, updatePayloadList);
 | 
						|
    adjustSection();
 | 
						|
    refreshSearch();
 | 
						|
  } catch (err) {
 | 
						|
    console.log(err);
 | 
						|
    PingPicker._showRawPingData();
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function displayRichPingData(ping, updatePayloadList) {
 | 
						|
  // Update the payload list and store lists
 | 
						|
  if (updatePayloadList) {
 | 
						|
    renderStoreList(ping.payload);
 | 
						|
  }
 | 
						|
 | 
						|
  // Show general data.
 | 
						|
  GeneralData.render(ping);
 | 
						|
 | 
						|
  // Show environment data.
 | 
						|
  EnvironmentData.render(ping);
 | 
						|
 | 
						|
  RawPayloadData.render(ping);
 | 
						|
 | 
						|
  // We have special rendering code for the payloads from "main" and "event" pings.
 | 
						|
  // For any other pings we just render the raw JSON payload.
 | 
						|
  let isMainPing = ping.type == "main" || ping.type == "saved-session";
 | 
						|
  let isEventPing = ping.type == "event";
 | 
						|
  togglePingSections(isMainPing);
 | 
						|
 | 
						|
  if (isEventPing) {
 | 
						|
    // Copy the payload, so we don't modify the raw representation
 | 
						|
    // Ensure we always have at least the parent process.
 | 
						|
    let payload = { processes: { parent: {} } };
 | 
						|
    for (let process of Object.keys(ping.payload.events)) {
 | 
						|
      payload.processes[process] = {
 | 
						|
        events: ping.payload.events[process],
 | 
						|
      };
 | 
						|
    }
 | 
						|
 | 
						|
    // We transformed the actual payload, let's reload the store list if necessary.
 | 
						|
    if (updatePayloadList) {
 | 
						|
      renderStoreList(payload);
 | 
						|
    }
 | 
						|
 | 
						|
    // Show event data.
 | 
						|
    Events.render(payload);
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  if (!isMainPing) {
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  // Show slow SQL stats
 | 
						|
  SlowSQL.render(ping);
 | 
						|
 | 
						|
  // Render Addon details.
 | 
						|
  AddonDetails.render(ping);
 | 
						|
 | 
						|
  let payload = ping.payload;
 | 
						|
  // Show basic session info gathered
 | 
						|
  SessionInformation.render(payload);
 | 
						|
 | 
						|
  // Show scalar data.
 | 
						|
  Scalars.render(payload);
 | 
						|
  KeyedScalars.render(payload);
 | 
						|
 | 
						|
  // Show histogram data
 | 
						|
  HistogramSection.render(payload);
 | 
						|
 | 
						|
  // Show keyed histogram data
 | 
						|
  KeyedHistogramSection.render(payload);
 | 
						|
 | 
						|
  // Show event data.
 | 
						|
  Events.render(payload);
 | 
						|
 | 
						|
  LateWritesSingleton.renderLateWrites(payload.lateWrites);
 | 
						|
 | 
						|
  // Show simple measurements
 | 
						|
  SimpleMeasurements.render(payload);
 | 
						|
}
 | 
						|
 | 
						|
window.addEventListener("load", onLoad);
 |