/* 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
--> --------------------