Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Firefox/toolkit/content/   (Browser von der Mozilla Stiftung Version 136.0.1©)  Datei vom 10.2.2025 mit Größe 76 kB image not shown  

Quelle  aboutTelemetry.js   Sprache: JAVA

 
/* 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();
--> --------------------

--> maximum size reached

--> --------------------

Messung V0.5
C=89 H=94 G=91

¤ Dauer der Verarbeitung: 0.22 Sekunden  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.