123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169 |
- /* 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';
- var Ci = Components.interfaces;
- var Cc = Components.classes;
- var Cu = Components.utils;
- Cu.import("resource://gre/modules/Services.jsm");
- Cu.import("resource://gre/modules/TelemetryTimestamps.jsm");
- Cu.import("resource://gre/modules/TelemetryController.jsm");
- Cu.import("resource://gre/modules/TelemetrySession.jsm");
- Cu.import("resource://gre/modules/TelemetryArchive.jsm");
- Cu.import("resource://gre/modules/TelemetryUtils.jsm");
- Cu.import("resource://gre/modules/TelemetryLog.jsm");
- Cu.import("resource://gre/modules/Preferences.jsm");
- Cu.import("resource://gre/modules/XPCOMUtils.jsm");
- Cu.import("resource://gre/modules/Task.jsm");
- const Telemetry = Services.telemetry;
- const bundle = Services.strings.createBundle(
- "chrome://global/locale/aboutTelemetry.properties");
- const brandBundle = Services.strings.createBundle(
- "chrome://branding/locale/brand.properties");
- // Maximum height of a histogram bar (in em for html, in chars for text)
- const MAX_BAR_HEIGHT = 18;
- 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 = "http://symbolapi.mozilla.org";
- 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 isArray(arg) {
- return Object.prototype.toString.call(arg) === '[object Array]';
- }
- function isFlatArray(obj) {
- if (!isArray(obj)) {
- return false;
- }
- return !obj.some(e => typeof(e) == "object");
- }
- /**
- * This is a helper function for explodeObject.
- */
- function flattenObject(obj, map, path, array) {
- if (!obj) {
- return;
- }
- 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, 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.indexOf(k) == -1) {
- 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.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIWebNavigation)
- .QueryInterface(Ci.nsIDocShellTreeItem)
- .rootTreeItem
- .QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDOMWindow);
- }
- /**
- * 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 && "openAdvancedPreferences" in mainWindow) {
- return mainWindow;
- }
- return null;
- }
- /**
- * Remove all child nodes of a document node.
- */
- function removeAllChildNodes(node) {
- while (node.hasChildNodes()) {
- node.removeChild(node.lastChild);
- }
- }
- /**
- * Pad a number to two digits with leading "0".
- */
- function padToTwoDigits(n) {
- return (n > 9) ? n: "0" + n;
- }
- /**
- * Return yesterdays date with the same time.
- */
- function yesterday(date) {
- let d = new Date(date);
- d.setDate(d.getDate() - 1);
- return d;
- }
- /**
- * This returns a short date string of the form YYYY/MM/DD.
- */
- function shortDateString(date) {
- return date.getFullYear()
- + "/" + padToTwoDigits(date.getMonth() + 1)
- + "/" + padToTwoDigits(date.getDate());
- }
- /**
- * This returns a short time string of the form hh:mm:ss.
- */
- function shortTimeString(date) {
- return padToTwoDigits(date.getHours())
- + ":" + padToTwoDigits(date.getMinutes())
- + ":" + padToTwoDigits(date.getSeconds());
- }
- var Settings = {
- SETTINGS: [
- // data upload
- {
- pref: PREF_FHR_UPLOAD_ENABLED,
- defaultPrefValue: false,
- descriptionEnabledId: "description-upload-enabled",
- descriptionDisabledId: "description-upload-disabled",
- },
- // extended "Telemetry" recording
- {
- pref: PREF_TELEMETRY_ENABLED,
- defaultPrefValue: false,
- descriptionEnabledId: "description-extended-recording-enabled",
- descriptionDisabledId: "description-extended-recording-disabled",
- },
- ],
- attachObservers: function() {
- for (let s of this.SETTINGS) {
- let setting = s;
- Preferences.observe(setting.pref, this.render, this);
- }
- let elements = document.getElementsByClassName("change-data-choices-link");
- for (let el of elements) {
- el.addEventListener("click", function() {
- // Show the data choices preferences on desktop.
- let mainWindow = getMainWindowWithPreferencesPane();
- mainWindow.openAdvancedPreferences("dataChoicesTab");
- }, false);
- }
- },
- detachObservers: function() {
- for (let setting of this.SETTINGS) {
- Preferences.ignore(setting.pref, this.render, this);
- }
- },
- /**
- * Updates the button & text at the top of the page to reflect Telemetry state.
- */
- render: function() {
- for (let setting of this.SETTINGS) {
- let enabledElement = document.getElementById(setting.descriptionEnabledId);
- let disabledElement = document.getElementById(setting.descriptionDisabledId);
- if (Preferences.get(setting.pref, setting.defaultPrefValue)) {
- enabledElement.classList.remove("hidden");
- disabledElement.classList.add("hidden");
- } else {
- enabledElement.classList.add("hidden");
- disabledElement.classList.remove("hidden");
- }
- }
- }
- };
- var PingPicker = {
- viewCurrentPingData: null,
- viewStructuredPingData: null,
- _archivedPings: null,
- attachObservers: function() {
- let elements = document.getElementsByName("choose-ping-source");
- for (let el of elements) {
- el.addEventListener("change", () => this.onPingSourceChanged(), false);
- }
- let displays = document.getElementsByName("choose-ping-display");
- for (let el of displays) {
- el.addEventListener("change", () => this.onPingDisplayChanged(), false);
- }
- document.getElementById("show-subsession-data").addEventListener("change", () => {
- this._updateCurrentPingData();
- });
- document.getElementById("choose-ping-week").addEventListener("change", () => {
- this._renderPingList();
- this._updateArchivedPingData();
- }, false);
- document.getElementById("choose-ping-id").addEventListener("change", () => {
- this._updateArchivedPingData()
- }, false);
- document.getElementById("newer-ping")
- .addEventListener("click", () => this._movePingIndex(-1), false);
- document.getElementById("older-ping")
- .addEventListener("click", () => this._movePingIndex(1), false);
- document.getElementById("choose-payload")
- .addEventListener("change", () => displayPingData(gPingData), false);
- document.getElementById("histograms-processes")
- .addEventListener("change", () => displayPingData(gPingData), false);
- document.getElementById("keyed-histograms-processes")
- .addEventListener("change", () => displayPingData(gPingData), false);
- },
- onPingSourceChanged: function() {
- this.update();
- },
- onPingDisplayChanged: function() {
- this.update();
- },
- update: Task.async(function*() {
- let viewCurrent = document.getElementById("ping-source-current").checked;
- let viewStructured = document.getElementById("ping-source-structured").checked;
- let currentChanged = viewCurrent !== this.viewCurrentPingData;
- let structuredChanged = viewStructured !== this.viewStructuredPingData;
- this.viewCurrentPingData = viewCurrent;
- this.viewStructuredPingData = viewStructured;
- // 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 = yield TelemetryArchive.promiseArchivedPingList();
- let sourceArchived = document.getElementById("ping-source-archive");
- sourceArchived.disabled = (archivedPingList.length == 0);
- if (currentChanged) {
- if (this.viewCurrentPingData) {
- document.getElementById("current-ping-picker").classList.remove("hidden");
- document.getElementById("archived-ping-picker").classList.add("hidden");
- this._updateCurrentPingData();
- } else {
- document.getElementById("current-ping-picker").classList.add("hidden");
- yield this._updateArchivedPingList(archivedPingList);
- document.getElementById("archived-ping-picker").classList.remove("hidden");
- }
- }
- if (structuredChanged) {
- if (this.viewStructuredPingData) {
- this._showStructuredPingData();
- } else {
- this._showRawPingData();
- }
- }
- }),
- _updateCurrentPingData: function() {
- const subsession = document.getElementById("show-subsession-data").checked;
- const ping = TelemetryController.getCurrentPingData(subsession);
- if (!ping) {
- return;
- }
- displayPingData(ping, true);
- },
- _updateArchivedPingData: function() {
- let id = this._getSelectedPingId();
- return TelemetryArchive.promiseArchivedPingById(id)
- .then((ping) => displayPingData(ping, true));
- },
- _updateArchivedPingList: Task.async(function*(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;
- // Collect the start dates for all the weeks we have pings for.
- let weekStart = (date) => {
- let weekDay = (date.getDay() + 6) % 7;
- let monday = new Date(date);
- monday.setDate(date.getDate() - weekDay);
- return TelemetryUtils.truncateToDays(monday);
- };
- let weekStartDates = new Set();
- for (let p of pingList) {
- weekStartDates.add(weekStart(new Date(p.timestampCreated)).getTime());
- }
- // Build a list of the week date ranges we have ping data for.
- let plusOneWeek = (date) => {
- let d = date;
- d.setDate(d.getDate() + 7);
- return d;
- };
- this._weeks = Array.from(weekStartDates.values(), startTime => ({
- startDate: new Date(startTime),
- endDate: plusOneWeek(new Date(startTime)),
- }));
- // Render the archive data.
- this._renderWeeks();
- this._renderPingList();
- // Update the displayed ping.
- yield this._updateArchivedPingData();
- }),
- _renderWeeks: function() {
- let weekSelector = document.getElementById("choose-ping-week");
- removeAllChildNodes(weekSelector);
- let index = 0;
- for (let week of this._weeks) {
- let text = shortDateString(week.startDate)
- + " - " + shortDateString(yesterday(week.endDate));
- let option = document.createElement("option");
- let content = document.createTextNode(text);
- option.appendChild(content);
- weekSelector.appendChild(option);
- }
- },
- _getSelectedWeek: function() {
- let weekSelector = document.getElementById("choose-ping-week");
- return this._weeks[weekSelector.selectedIndex];
- },
- _renderPingList: function(id = null) {
- let pingSelector = document.getElementById("choose-ping-id");
- removeAllChildNodes(pingSelector);
- let weekRange = this._getSelectedWeek();
- let pings = this._archivedPings.filter(
- (p) => p.timestampCreated >= weekRange.startDate.getTime() &&
- p.timestampCreated < weekRange.endDate.getTime());
- for (let p of pings) {
- let date = new Date(p.timestampCreated);
- let text = shortDateString(date)
- + " " + shortTimeString(date)
- + " - " + p.type;
- let option = document.createElement("option");
- let content = document.createTextNode(text);
- option.appendChild(content);
- option.setAttribute("value", p.id);
- if (id && p.id == id) {
- option.selected = true;
- }
- pingSelector.appendChild(option);
- }
- },
- _getSelectedPingId: function() {
- let pingSelector = document.getElementById("choose-ping-id");
- let selected = pingSelector.selectedOptions.item(0);
- return selected.getAttribute("value");
- },
- _movePingIndex: function(offset) {
- const id = this._getSelectedPingId();
- const index = this._archivedPings.findIndex((p) => p.id == id);
- const newIndex = Math.min(Math.max(index + offset, 0), this._archivedPings.length - 1);
- const ping = this._archivedPings[newIndex];
- const weekIndex = this._weeks.findIndex(
- (week) => ping.timestampCreated >= week.startDate.getTime() &&
- ping.timestampCreated < week.endDate.getTime());
- const options = document.getElementById("choose-ping-week").options;
- options.item(weekIndex).selected = true;
- this._renderPingList(ping.id);
- this._updateArchivedPingData();
- },
- _showRawPingData: function() {
- document.getElementById("raw-ping-data-section").classList.remove("hidden");
- document.getElementById("structured-ping-data-section").classList.add("hidden");
- },
- _showStructuredPingData: function() {
- document.getElementById("raw-ping-data-section").classList.add("hidden");
- document.getElementById("structured-ping-data-section").classList.remove("hidden");
- },
- };
- var GeneralData = {
- /**
- * Renders the general data
- */
- render: function(aPing) {
- setHasData("general-data-section", true);
- let table = document.createElement("table");
- let caption = document.createElement("caption");
- let captionString = bundle.GetStringFromName("generalDataTitle");
- caption.appendChild(document.createTextNode(captionString + "\n"));
- table.appendChild(caption);
- let headings = document.createElement("tr");
- this.appendColumn(headings, "th", bundle.GetStringFromName("generalDataHeadingName") + "\t");
- this.appendColumn(headings, "th", bundle.GetStringFromName("generalDataHeadingValue") + "\t");
- table.appendChild(headings);
- // The payload & environment parts are handled by other renderers.
- let ignoreSections = ["payload", "environment"];
- let data = explodeObject(filterObject(aPing, ignoreSections));
- for (let [path, value] of data) {
- let row = document.createElement("tr");
- this.appendColumn(row, "td", path + "\t");
- this.appendColumn(row, "td", value + "\t");
- table.appendChild(row);
- }
- let dataDiv = document.getElementById("general-data");
- removeAllChildNodes(dataDiv);
- dataDiv.appendChild(table);
- },
- /**
- * 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: function(aRowElement, aColType, aColText) {
- let colElement = document.createElement(aColType);
- let colTextElement = document.createTextNode(aColText);
- colElement.appendChild(colTextElement);
- aRowElement.appendChild(colElement);
- },
- };
- var EnvironmentData = {
- /**
- * Renders the environment data
- */
- render: function(ping) {
- let dataDiv = document.getElementById("environment-data");
- removeAllChildNodes(dataDiv);
- const hasData = !!ping.environment;
- setHasData("environment-data-section", hasData);
- if (!hasData) {
- return;
- }
- let data = sectionalizeObject(ping.environment);
- for (let [section, sectionData] of data) {
- if (section == "addons") {
- break;
- }
- let table = document.createElement("table");
- this.appendHeading(table);
- for (let [path, value] of sectionData) {
- let row = document.createElement("tr");
- this.appendColumn(row, "td", path);
- this.appendColumn(row, "td", value);
- table.appendChild(row);
- }
- let hasData = sectionData.size > 0;
- this.createSubsection(section, hasData, table, dataDiv);
- }
- // We use specialized rendering here to make the addon and plugin listings
- // more readable.
- this.createAddonSection(dataDiv, ping);
- },
- createSubsection: function(title, hasSubdata, subSectionData, dataDiv) {
- let dataSection = document.createElement("section");
- dataSection.classList.add("data-subsection");
- if (hasSubdata) {
- dataSection.classList.add("has-subdata");
- }
- // Create section heading
- let sectionName = document.createElement("h2");
- sectionName.setAttribute("class", "section-name");
- sectionName.appendChild(document.createTextNode(title));
- sectionName.addEventListener("click", toggleSection, false);
- // Create caption for toggling the subsection visibility.
- let toggleCaption = document.createElement("span");
- toggleCaption.setAttribute("class", "toggle-caption");
- let toggleText = bundle.GetStringFromName("environmentDataSubsectionToggle");
- toggleCaption.appendChild(document.createTextNode(" " + toggleText));
- toggleCaption.addEventListener("click", toggleSection, false);
- // Create caption for empty subsections.
- let emptyCaption = document.createElement("span");
- emptyCaption.setAttribute("class", "empty-caption");
- let emptyText = bundle.GetStringFromName("environmentDataSubsectionEmpty");
- emptyCaption.appendChild(document.createTextNode(" " + emptyText));
- // Create data container
- let data = document.createElement("div");
- data.setAttribute("class", "subsection-data subdata");
- data.appendChild(subSectionData);
- // Append elements
- dataSection.appendChild(sectionName);
- dataSection.appendChild(toggleCaption);
- dataSection.appendChild(emptyCaption);
- dataSection.appendChild(data);
- dataDiv.appendChild(dataSection);
- },
- renderPersona: function(addonObj, addonSection, sectionTitle) {
- let table = document.createElement("table");
- table.setAttribute("id", sectionTitle);
- this.appendAddonSubsectionTitle(sectionTitle, table);
- this.appendRow(table, "persona", addonObj.persona);
- addonSection.appendChild(table);
- },
- renderActivePlugins: function(addonObj, addonSection, sectionTitle) {
- let data = explodeObject(addonObj);
- let table = document.createElement("table");
- table.setAttribute("id", sectionTitle);
- this.appendAddonSubsectionTitle(sectionTitle, table);
- for (let plugin of addonObj) {
- let data = explodeObject(plugin);
- this.appendHeadingName(table, data.get("name"));
- for (let [key, value] of data) {
- this.appendRow(table, key, value);
- }
- }
- addonSection.appendChild(table);
- },
- renderAddonsObject: function(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: function(addonObj, addonSection, sectionTitle) {
- let data = explodeObject(addonObj);
- let table = document.createElement("table");
- table.setAttribute("class", sectionTitle);
- this.appendAddonSubsectionTitle(sectionTitle, table);
- this.appendHeading(table);
- for (let [key, value] of data) {
- this.appendRow(table, key, value);
- }
- addonSection.appendChild(table);
- },
- appendAddonID: function(table, addonID) {
- this.appendRow(table, "id", addonID);
- },
- appendHeading: function(table) {
- let headings = document.createElement("tr");
- this.appendColumn(headings, "th", bundle.GetStringFromName("environmentDataHeadingName"));
- this.appendColumn(headings, "th", bundle.GetStringFromName("environmentDataHeadingValue"));
- table.appendChild(headings);
- },
- appendHeadingName: function(table, name) {
- let headings = document.createElement("tr");
- this.appendColumn(headings, "th", name);
- headings.cells[0].colSpan = 2;
- table.appendChild(headings);
- },
- appendAddonSubsectionTitle: function(section, table) {
- let caption = document.createElement("caption");
- caption.setAttribute("class", "addon-caption");
- caption.appendChild(document.createTextNode(section));
- table.appendChild(caption);
- },
- createAddonSection: function(dataDiv, ping) {
- let addonSection = document.createElement("div");
- let addons = ping.environment.addons;
- this.renderAddonsObject(addons.activeAddons, addonSection, "activeAddons");
- this.renderActivePlugins(addons.activePlugins, addonSection, "activePlugins");
- this.renderKeyValueObject(addons.theme, addonSection, "theme");
- this.renderKeyValueObject(addons.activeExperiment, addonSection, "activeExperiment");
- this.renderAddonsObject(addons.activeGMPlugins, addonSection, "activeGMPlugins");
- this.renderPersona(addons, addonSection, "persona");
- let hasAddonData = Object.keys(ping.environment.addons).length > 0;
- this.createSubsection("addons", hasAddonData, addonSection, dataDiv);
- },
- appendRow: function(table, id, value) {
- let row = document.createElement("tr");
- 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: function(aRowElement, aColType, aColText) {
- let colElement = document.createElement(aColType);
- let colTextElement = document.createTextNode(aColText);
- colElement.appendChild(colTextElement);
- aRowElement.appendChild(colElement);
- },
- };
- var TelLog = {
- /**
- * Renders the telemetry log
- */
- render: function(aPing) {
- let entries = aPing.payload.log;
- const hasData = entries && entries.length > 0;
- setHasData("telemetry-log-section", hasData);
- if (!hasData) {
- return;
- }
- let table = document.createElement("table");
- let caption = document.createElement("caption");
- let captionString = bundle.GetStringFromName("telemetryLogTitle");
- caption.appendChild(document.createTextNode(captionString + "\n"));
- table.appendChild(caption);
- let headings = document.createElement("tr");
- this.appendColumn(headings, "th", bundle.GetStringFromName("telemetryLogHeadingId") + "\t");
- this.appendColumn(headings, "th", bundle.GetStringFromName("telemetryLogHeadingTimestamp") + "\t");
- this.appendColumn(headings, "th", bundle.GetStringFromName("telemetryLogHeadingData") + "\t");
- table.appendChild(headings);
- for (let entry of entries) {
- let row = document.createElement("tr");
- for (let elem of entry) {
- this.appendColumn(row, "td", elem + "\t");
- }
- table.appendChild(row);
- }
- let dataDiv = document.getElementById("telemetry-log");
- removeAllChildNodes(dataDiv);
- dataDiv.appendChild(table);
- },
- /**
- * 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: function(aRowElement, aColType, aColText) {
- let colElement = document.createElement(aColType);
- let colTextElement = document.createTextNode(aColText);
- colElement.appendChild(colTextElement);
- aRowElement.appendChild(colElement);
- },
- };
- var SlowSQL = {
- slowSqlHits: bundle.GetStringFromName("slowSqlHits"),
- slowSqlAverage: bundle.GetStringFromName("slowSqlAverage"),
- slowSqlStatement: bundle.GetStringFromName("slowSqlStatement"),
- mainThreadTitle: bundle.GetStringFromName("slowSqlMain"),
- otherThreadTitle: bundle.GetStringFromName("slowSqlOther"),
- /**
- * 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").classList.remove("hidden");
- }
- let slowSqlDiv = document.getElementById("slow-sql-tables");
- removeAllChildNodes(slowSqlDiv);
- // Main thread
- if (mainThreadCount > 0) {
- let table = document.createElement("table");
- this.renderTableHeader(table, this.mainThreadTitle);
- this.renderTable(table, mainThread);
- slowSqlDiv.appendChild(table);
- slowSqlDiv.appendChild(document.createElement("hr"));
- }
- // Other threads
- if (otherThreadCount > 0) {
- let table = document.createElement("table");
- this.renderTableHeader(table, this.otherThreadTitle);
- this.renderTable(table, otherThreads);
- slowSqlDiv.appendChild(table);
- slowSqlDiv.appendChild(document.createElement("hr"));
- }
- },
- /**
- * 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, aTitle) {
- let caption = document.createElement("caption");
- caption.appendChild(document.createTextNode(aTitle + "\n"));
- aTable.appendChild(caption);
- let headings = document.createElement("tr");
- this.appendColumn(headings, "th", this.slowSqlHits + "\t");
- this.appendColumn(headings, "th", this.slowSqlAverage + "\t");
- this.appendColumn(headings, "th", this.slowSqlStatement + "\n");
- 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);
- let colTextElement = document.createTextNode(aColText);
- colElement.appendChild(colTextElement);
- aRowElement.appendChild(colElement);
- }
- };
- var StackRenderer = {
- stackTitle: bundle.GetStringFromName("stackTitle"),
- memoryMapTitle: bundle.GetStringFromName("memoryMapTitle"),
- /**
- * Outputs the memory map associated with this hang report
- *
- * @param aDiv Output div
- */
- renderMemoryMap: function StackRenderer_renderMemoryMap(aDiv, memoryMap) {
- aDiv.appendChild(document.createTextNode(this.memoryMapTitle));
- 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) {
- aDiv.appendChild(document.createTextNode(this.stackTitle));
- 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 + '-data');
- removeAllChildNodes(div);
- let fetchE = document.getElementById(aPrefix + '-fetch-symbols');
- if (fetchE) {
- fetchE.classList.remove("hidden");
- }
- let hideE = document.getElementById(aPrefix + '-hide-symbols');
- if (hideE) {
- hideE.classList.add("hidden");
- }
- if (aStacks.length == 0) {
- 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 aFormatArgs formating args to be passed to formatStringFromName.
- */
- renderHeader: function StackRenderer_renderHeader(aPrefix, aFormatArgs) {
- let div = document.getElementById(aPrefix + "-data");
- let titleElement = document.createElement("span");
- titleElement.className = "stack-title";
- let titleText = bundle.formatStringFromName(
- aPrefix + "-title", aFormatArgs, aFormatArgs.length);
- titleElement.appendChild(document.createTextNode(titleText));
- div.appendChild(titleElement);
- div.appendChild(document.createElement("br"));
- }
- };
- var RawPayload = {
- /**
- * Renders the raw payload
- */
- render: function(aPing) {
- setHasData("raw-payload-section", true);
- let pre = document.getElementById("raw-payload-data-pre");
- pre.textContent = JSON.stringify(aPing.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 =
- function SymbolicationRequest_handleSymbolResponse() {
- if (this.symbolRequest.readyState != 4)
- return;
- let fetchElement = document.getElementById(this.prefix + "-fetch-symbols");
- fetchElement.classList.add("hidden");
- let hideElement = document.getElementById(this.prefix + "-hide-symbols");
- hideElement.classList.remove("hidden");
- let div = document.getElementById(this.prefix + "-data");
- removeAllChildNodes(div);
- let errorMessage = bundle.GetStringFromName("errorFetchingSymbols");
- 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 ChromeHangs = {
- symbolRequest: null,
- /**
- * Renders raw chrome hang data
- */
- render: function ChromeHangs_render(aPing) {
- let hangs = aPing.payload.chromeHangs;
- setHasData("chrome-hangs-section", !!hangs);
- if (!hangs) {
- return;
- }
- let stacks = hangs.stacks;
- let memoryMap = hangs.memoryMap;
- let durations = hangs.durations;
- StackRenderer.renderStacks("chrome-hangs", stacks, memoryMap,
- (index) => this.renderHangHeader(index, durations));
- },
- renderHangHeader: function ChromeHangs_renderHangHeader(aIndex, aDurations) {
- StackRenderer.renderHeader("chrome-hangs", [aIndex + 1, aDurations[aIndex]]);
- }
- };
- var ThreadHangStats = {
- /**
- * Renders raw thread hang stats data
- */
- render: function(aPayload) {
- let div = document.getElementById("thread-hang-stats");
- removeAllChildNodes(div);
- let stats = aPayload.threadHangStats;
- setHasData("thread-hang-stats-section", stats && (stats.length > 0));
- if (!stats) {
- return;
- }
- stats.forEach((thread) => {
- div.appendChild(this.renderThread(thread));
- });
- },
- /**
- * Creates and fills data corresponding to a thread
- */
- renderThread: function(aThread) {
- let div = document.createElement("div");
- let title = document.createElement("h2");
- title.textContent = aThread.name;
- div.appendChild(title);
- // Don't localize the histogram name, because the
- // name is also used as the div element's ID
- Histogram.render(div, aThread.name + "-Activity",
- aThread.activity, {exponential: true}, true);
- aThread.hangs.forEach((hang, index) => {
- let hangName = aThread.name + "-Hang-" + (index + 1);
- let hangDiv = Histogram.render(
- div, hangName, hang.histogram, {exponential: true}, true);
- let stackDiv = document.createElement("div");
- let stack = hang.nativeStack || hang.stack;
- stack.forEach((frame) => {
- stackDiv.appendChild(document.createTextNode(frame));
- // Leave an extra <br> at the end of the stack listing
- stackDiv.appendChild(document.createElement("br"));
- });
- // Insert stack after the histogram title
- hangDiv.insertBefore(stackDiv, hangDiv.childNodes[1]);
- });
- return div;
- },
- };
- var Histogram = {
- hgramSamplesCaption: bundle.GetStringFromName("histogramSamples"),
- hgramAverageCaption: bundle.GetStringFromName("histogramAverage"),
- hgramSumCaption: bundle.GetStringFromName("histogramSum"),
- hgramCopyCaption: bundle.GetStringFromName("histogramCopy"),
- /**
- * 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
- * @param aIsBHR whether or not requires fixing the labels for TimeHistogram
- */
- render: function Histogram_render(aParent, aName, aHgram, aOptions, aIsBHR) {
- let options = aOptions || {};
- let hgram = this.processHistogram(aHgram, aName, aIsBHR);
- let outerDiv = document.createElement("div");
- outerDiv.className = "histogram";
- outerDiv.id = aName;
- let divTitle = document.createElement("div");
- divTitle.className = "histogram-title";
- divTitle.appendChild(document.createTextNode(aName));
- outerDiv.appendChild(divTitle);
- let stats = hgram.sample_count + " " + this.hgramSamplesCaption + ", " +
- this.hgramAverageCaption + " = " + hgram.pretty_average + ", " +
- this.hgramSumCaption + " = " + hgram.sum;
- let divStats = document.createElement("div");
- divStats.appendChild(document.createTextNode(stats));
- outerDiv.appendChild(divStats);
- if (isRTL()) {
- hgram.buckets.reverse();
- 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";
- copyButton.appendChild(document.createTextNode(this.hgramCopyCaption));
- copyButton.histogramText = aName + EOL + stats + EOL + EOL + textData;
- copyButton.addEventListener("click", function() {
- Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper)
- .copyString(this.histogramText);
- });
- outerDiv.appendChild(copyButton);
- aParent.appendChild(outerDiv);
- return outerDiv;
- },
- processHistogram: function(aHgram, aName, aIsBHR) {
- 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);
- function labelFunc(k) {
- // - BHR histograms are TimeHistograms: Exactly power-of-two buckets (from 0)
- // (buckets: [0..1], [2..3], [4..7], [8..15], ... note the 0..1 anomaly - same bucket)
- // - TimeHistogram's JS representation adds a dummy (empty) "0" bucket, and
- // the rest of the buckets have the label as the upper value of the
- // bucket (non TimeHistograms have the lower value of the bucket as label).
- // So JS TimeHistograms bucket labels are: 0 (dummy), 1, 3, 7, 15, ...
- // - see toolkit/components/telemetry/Telemetry.cpp
- // (CreateJSTimeHistogram, CreateJSThreadHangStats, CreateJSHangHistogram)
- // - see toolkit/components/telemetry/ThreadHangStats.h
- // Fix BHR labels to the "standard" format for about:telemetry as follows:
- // - The dummy 0 label+bucket will be filtered before arriving here
- // - If it's 1 -> manually correct it to 0 (the 0..1 anomaly)
- // - For the rest, set the label as the bottom value instead of the upper.
- // --> so we'll end with the following (non dummy) labels: 0, 2, 4, 8, 16, ...
- if (!aIsBHR) {
- return k;
- }
- return k == 1 ? 0 : (k + 1) / 2;
- }
- const labelledValues = Object.keys(aHgram.values)
- .filter(label => !aIsBHR || Number(label) != 0) // remove dummy 0 label for BHR
- .map(k => [labelFunc(Number(k)), aHgram.values[k]]);
- let result = {
- values: labelledValues,
- pretty_average: average,
- max: max_value,
- sample_count: 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: function(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) {
- 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 - String(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 bucket label
- barDiv.appendChild(document.createTextNode(label));
- aDiv.appendChild(barDiv);
- }
- return text.substr(EOL.length); // Trim the EOL before the first line
- },
- /**
- * Helper function for filtering histogram elements by their id
- * Adds the "filter-blocked" class to histogram nodes whose IDs don't match the filter.
- *
- * @param aContainerNode Container node containing the histogram class nodes to filter
- * @param aFilterText either text or /RegEx/. If text, case-insensitive and AND words
- */
- filterHistograms: function _filterHistograms(aContainerNode, aFilterText) {
- let filter = aFilterText.toString();
- // Pass if: all non-empty array items match (case-sensitive)
- function isPassText(subject, filter) {
- for (let item of filter) {
- if (item.length && subject.indexOf(item) < 0) {
- return false; // mismatch and not a spurious space
- }
- }
- return true;
- }
- function isPassRegex(subject, filter) {
- return filter.test(subject);
- }
- // 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 = isPassText;
- filter = filter.toLowerCase().split(" ");
- } else {
- isPassFunc = 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;
- };
- }
- }
- let needLower = (isPassFunc === isPassText);
- let histograms = aContainerNode.getElementsByClassName("histogram");
- for (let hist of histograms) {
- hist.classList[isPassFunc((needLower ? hist.id.toLowerCase() : hist.id), filter) ? "remove" : "add"]("filter-blocked");
- }
- },
- /**
- * Event handler for change at histograms filter input
- *
- * When invoked, 'this' is expected to be the filter HTML node.
- */
- histogramFilterChanged: function _histogramFilterChanged() {
- if (this.idleTimeout) {
- clearTimeout(this.idleTimeout);
- }
- this.idleTimeout = setTimeout( () => {
- Histogram.filterHistograms(document.getElementById(this.getAttribute("target_id")), this.value);
- }, FILTER_IDLE_TIMEOUT);
- }
- };
- /*
- * 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 == 0) {
- 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 == 0) {
- 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 KeyValueTable = {
- /**
- * Returns a 2-column table with keys and values
- * @param aMeasurements Each key in this JS object is rendered as a row in
- * the table with its corresponding value
- * @param aKeysLabel Column header for the keys column
- * @param aValuesLabel Column header for the values column
- */
- render: function KeyValueTable_render(aMeasurements, aKeysLabel, aValuesLabel) {
- let table = document.createElement("table");
- this.renderHeader(table, aKeysLabel, aValuesLabel);
- this.renderBody(table, aMeasurements);
- return table;
- },
- /**
- * Create the table header
- * Tabs & newlines added to cells to make it easier to copy-paste.
- *
- * @param aTable Table element
- * @param aKeysLabel Column header for the keys column
- * @param aValuesLabel Column header for the values column
- */
- renderHeader: function KeyValueTable_renderHeader(aTable, aKeysLabel, aValuesLabel) {
- let headerRow = document.createElement("tr");
- aTable.appendChild(headerRow);
- let keysColumn = document.createElement("th");
- keysColumn.appendChild(document.createTextNode(aKeysLabel + "\t"));
- let valuesColumn = document.createElement("th");
- valuesColumn.appendChild(document.createTextNode(aValuesLabel + "\n"));
- headerRow.appendChild(keysColumn);
- headerRow.appendChild(valuesColumn);
- },
- /**
- * Create the table body
- * Tabs & newlines added to cells to make it easier to copy-paste.
- *
- * @param aTable Table element
- * @param aMeasurements Key/value map
- */
- renderBody: function KeyValueTable_renderBody(aTable, aMeasurements) {
- for (let [key, value] of Object.entries(aMeasurements)) {
- // use .valueOf() to unbox Number, String, etc. objects
- if (value &&
- (typeof value == "object") &&
- (typeof value.valueOf() == "object")) {
- value = RenderObject(value);
- }
- let newRow = document.createElement("tr");
- aTable.appendChild(newRow);
- let keyField = document.createElement("td");
- keyField.appendChild(document.createTextNode(key + "\t"));
- newRow.appendChild(keyField);
- let valueField = document.createElement("td");
- valueField.appendChild(document.createTextNode(value + "\n"));
- newRow.appendChild(valueField);
- }
- }
- };
- var GenericTable = {
- /**
- * 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: function(rows, headings) {
- 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: function(table, headings) {
- let headerRow = document.createElement("tr");
- table.appendChild(headerRow);
- for (let i = 0; i < headings.length; ++i) {
- let suffix = (i == (headings.length - 1)) ? "\n" : "\t";
- let column = document.createElement("th");
- column.appendChild(document.createTextNode(headings[i] + suffix));
- 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: function(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");
- 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: function(parent, id, keyedHistogram) {
- let outerDiv = document.createElement("div");
- outerDiv.className = "keyed-histogram";
- outerDiv.id = id;
- let divTitle = document.createElement("div");
- divTitle.className = "keyed-histogram-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 = {
- tableIDTitle: bundle.GetStringFromName("addonTableID"),
- tableDetailsTitle: bundle.GetStringFromName("addonTableDetails"),
- /**
- * 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: function AddonDetails_render(aPing) {
- let addonSection = document.getElementById("addon-details");
- removeAllChildNodes(addonSection);
- let addonDetails = aPing.payload.addonDetails;
- const hasData = addonDetails && Object.keys(addonDetails).length > 0;
- setHasData("addon-details-section", hasData);
- if (!hasData) {
- return;
- }
- for (let provider in addonDetails) {
- let providerSection = document.createElement("h2");
- let titleText = bundle.formatStringFromName("addonProvider", [provider], 1);
- providerSection.appendChild(document.createTextNode(titleText));
- addonSection.appendChild(providerSection);
- addonSection.appendChild(
- KeyValueTable.render(addonDetails[provider],
- this.tableIDTitle, this.tableDetailsTitle));
- }
- }
- };
- var Scalars = {
- /**
- * 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.
- */
- render: function(aPayload) {
- let scalarsSection = document.getElementById("scalars");
- removeAllChildNodes(scalarsSection);
- if (!aPayload.processes || !aPayload.processes.parent) {
- return;
- }
- let scalars = aPayload.processes.parent.scalars;
- const hasData = scalars && Object.keys(scalars).length > 0;
- setHasData("scalars-section", hasData);
- if (!hasData) {
- return;
- }
- const headingName = bundle.GetStringFromName("namesHeader");
- const headingValue = bundle.GetStringFromName("valuesHeader");
- const table = KeyValueTable.render(scalars, headingName, headingValue);
- scalarsSection.appendChild(table);
- }
- };
- var KeyedScalars = {
- /**
- * 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.
- */
- render: function(aPayload) {
- let scalarsSection = document.getElementById("keyed-scalars");
- removeAllChildNodes(scalarsSection);
- if (!aPayload.processes || !aPayload.processes.parent) {
- return;
- }
- let keyedScalars = aPayload.processes.parent.keyedScalars;
- const hasData = keyedScalars && Object.keys(keyedScalars).length > 0;
- setHasData("keyed-scalars-section", hasData);
- if (!hasData) {
- return;
- }
- const headingName = bundle.GetStringFromName("namesHeader");
- const headingValue = bundle.GetStringFromName("valuesHeader");
- for (let scalar in keyedScalars) {
- // Add the name of the scalar.
- let scalarNameSection = document.createElement("h2");
- scalarNameSection.appendChild(document.createTextNode(scalar));
- scalarsSection.appendChild(scalarNameSection);
- // Populate the section with the key-value pairs from the scalar.
- const table = KeyValueTable.render(keyedScalars[scalar], headingName, headingValue);
- scalarsSection.appendChild(table);
- }
- }
- };
- 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: function(aPayload) {
- let eventsSection = document.getElementById("events");
- removeAllChildNodes(eventsSection);
- if (!aPayload.processes || !aPayload.processes.parent) {
- return;
- }
- const events = aPayload.processes.parent.events;
- const hasData = events && Object.keys(events).length > 0;
- setHasData("events-section", hasData);
- if (!hasData) {
- return;
- }
- const headings = [
- "timestamp",
- "category",
- "method",
- "object",
- "value",
- "extra",
- ];
- const table = GenericTable.render(events, headings);
- eventsSection.appendChild(table);
- }
- };
- /**
- * 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");
- }
- /**
- * Helper function that expands and collapses sections +
- * changes caption on the toggle text
- */
- function toggleSection(aEvent) {
- let parentElement = aEvent.target.parentElement;
- if (!parentElement.classList.contains("has-data") &&
- !parentElement.classList.contains("has-subdata")) {
- return; // nothing to toggle
- }
- parentElement.classList.toggle("expanded");
- // Store section opened/closed state in a hidden checkbox (which is then used on reload)
- let statebox = parentElement.getElementsByClassName("statebox")[0];
- if (statebox) {
- statebox.checked = parentElement.classList.contains("expanded");
- }
- }
- /**
- * Sets the text of the page header based on a config pref + bundle strings
- */
- function setupPageHeader()
- {
- let serverOwner = Preferences.get(PREF_TELEMETRY_SERVER_OWNER, "Mozilla");
- let brandName = brandBundle.GetStringFromName("brandFullName");
- let subtitleText = bundle.formatStringFromName(
- "pageSubtitle", [serverOwner, brandName], 2);
- let subtitleElement = document.getElementById("page-subtitle");
- subtitleElement.appendChild(document.createTextNode(subtitleText));
- }
- /**
- * Initializes load/unload, pref change and mouse-click listeners
- */
- function setupListeners() {
- Settings.attachObservers();
- PingPicker.attachObservers();
- // Clean up observers when page is closed
- window.addEventListener("unload",
- function unloadHandler(aEvent) {
- window.removeEventListener("unload", unloadHandler);
- Settings.detachObservers();
- }, false);
- document.getElementById("chrome-hangs-fetch-symbols").addEventListener("click",
- function () {
- if (!gPingData) {
- return;
- }
- let hangs = gPingData.payload.chromeHangs;
- let req = new SymbolicationRequest("chrome-hangs",
- ChromeHangs.renderHangHeader,
- hangs.memoryMap,
- hangs.stacks,
- hangs.durations);
- req.fetchSymbols();
- }, false);
- document.getElementById("chrome-hangs-hide-symbols").addEventListener("click",
- function () {
- if (!gPingData) {
- return;
- }
- ChromeHangs.render(gPingData);
- }, false);
- document.getElementById("late-writes-fetch-symbols").addEventListener("click",
- function () {
- if (!gPingData) {
- return;
- }
- let lateWrites = gPingData.payload.lateWrites;
- let req = new SymbolicationRequest("late-writes",
- LateWritesSingleton.renderHeader,
- lateWrites.memoryMap,
- lateWrites.stacks);
- req.fetchSymbols();
- }, false);
- document.getElementById("late-writes-hide-symbols").addEventListener("click",
- function () {
- if (!gPingData) {
- return;
- }
- LateWritesSingleton.renderLateWrites(gPingData.payload.lateWrites);
- }, false);
- // Clicking on the section name will toggle its state
- let sectionHeaders = document.getElementsByClassName("section-name");
- for (let sectionHeader of sectionHeaders) {
- sectionHeader.addEventListener("click", toggleSection, false);
- }
- // Clicking on the "toggle" text will also toggle section's state
- let toggleLinks = document.getElementsByClassName("toggle-caption");
- for (let toggleLink of toggleLinks) {
- toggleLink.addEventListener("click", toggleSection, false);
- }
- }
- function onLoad() {
- window.removeEventListener("load", onLoad);
- // Set the text in the page header
- setupPageHeader();
- // Set up event listeners
- setupListeners();
- // Render settings.
- Settings.render();
- // Restore sections states
- let stateboxes = document.getElementsByClassName("statebox");
- for (let box of stateboxes) {
- if (box.checked) { // Was open. Will still display as empty if not has-data
- box.parentElement.classList.add("expanded");
- }
- }
- // Update ping data when async Telemetry init is finished.
- Telemetry.asyncFetchTelemetryData(() => PingPicker.update());
- }
- var LateWritesSingleton = {
- renderHeader: function LateWritesSingleton_renderHeader(aIndex) {
- StackRenderer.renderHeader("late-writes", [aIndex + 1]);
- },
- renderLateWrites: function LateWritesSingleton_renderLateWrites(lateWrites) {
- setHasData("late-writes-section", !!lateWrites);
- if (!lateWrites) {
- return;
- }
- let stacks = lateWrites.stacks;
- let memoryMap = lateWrites.memoryMap;
- StackRenderer.renderStacks('late-writes', stacks, memoryMap,
- LateWritesSingleton.renderHeader);
- }
- };
- /**
- * Helper function for sorting the startup milestones in the Simple Measurements
- * section into temporal order.
- *
- * @param aSimpleMeasurements Telemetry ping's "Simple Measurements" data
- * @return Sorted measurements
- */
- function sortStartupMilestones(aSimpleMeasurements) {
- const telemetryTimestamps = TelemetryTimestamps.get();
- let startupEvents = Services.startup.getStartupInfo();
- delete startupEvents['process'];
- function keyIsMilestone(k) {
- return (k in startupEvents) || (k in telemetryTimestamps);
- }
- let sortedKeys = Object.keys(aSimpleMeasurements);
- // Sort the measurements, with startup milestones at the front + ordered by time
- sortedKeys.sort(function keyCompare(keyA, keyB) {
- let isKeyAMilestone = keyIsMilestone(keyA);
- let isKeyBMilestone = keyIsMilestone(keyB);
- // First order by startup vs non-startup measurement
- if (isKeyAMilestone && !isKeyBMilestone)
- return -1;
- if (!isKeyAMilestone && isKeyBMilestone)
- return 1;
- // Don't change order of non-startup measurements
- if (!isKeyAMilestone && !isKeyBMilestone)
- return 0;
- // If both keys are startup measurements, order them by value
- return aSimpleMeasurements[keyA] - aSimpleMeasurements[keyB];
- });
- // Insert measurements into a result object in sort-order
- let result = {};
- for (let key of sortedKeys) {
- result[key] = aSimpleMeasurements[key];
- }
- return result;
- }
- function renderProcessList(ping, selectEl) {
- removeAllChildNodes(selectEl);
- let option = document.createElement("option");
- option.appendChild(document.createTextNode("parent"));
- option.setAttribute("value", "");
- option.selected = true;
- selectEl.appendChild(option);
- if (!("processes" in ping.payload)) {
- selectEl.disabled = true;
- return;
- }
- selectEl.disabled = false;
- for (let process of Object.keys(ping.payload.processes)) {
- // TODO: parent hgrams are on root payload, not in payload.processes.parent
- // When/If that gets moved, you'll need to remove this:
- if (process === "parent") {
- continue;
- }
- option = document.createElement("option");
- option.appendChild(document.createTextNode(process));
- option.setAttribute("value", process);
- selectEl.appendChild(option);
- }
- }
- function renderPayloadList(ping) {
- // Rebuild the payload select with options:
- // Parent Payload (selected)
- // Child Payload 1..ping.payload.childPayloads.length
- let listEl = document.getElementById("choose-payload");
- removeAllChildNodes(listEl);
- let option = document.createElement("option");
- let text = bundle.GetStringFromName("parentPayload");
- let content = document.createTextNode(text);
- let payloadIndex = 0;
- option.appendChild(content);
- option.setAttribute("value", payloadIndex++);
- option.selected = true;
- listEl.appendChild(option);
- if (!ping.payload.childPayloads) {
- listEl.disabled = true;
- return
- }
- listEl.disabled = false;
- for (; payloadIndex <= ping.payload.childPayloads.length; ++payloadIndex) {
- option = document.createElement("option");
- text = bundle.formatStringFromName("childPayloadN", [payloadIndex], 1);
- content = document.createTextNode(text);
- option.appendChild(content);
- option.setAttribute("value", payloadIndex);
- listEl.appendChild(option);
- }
- }
- function toggleElementHidden(element, isHidden) {
- if (isHidden) {
- element.classList.add("hidden");
- } else {
- element.classList.remove("hidden");
- }
- }
- function togglePingSections(isMainPing) {
- // We always show the sections that are "common" to all pings.
- // The raw payload section is only used for pings other than "main" and "saved-session".
- let commonSections = new Set(["general-data-section", "environment-data-section"]);
- let otherPingSections = new Set(["raw-payload-section"]);
- let elements = document.getElementById("structured-ping-data-section").children;
- for (let section of elements) {
- if (commonSections.has(section.id)) {
- continue;
- }
- let showElement = isMainPing != otherPingSections.has(section.id);
- toggleElementHidden(section, !showElement);
- }
- }
- function displayPingData(ping, updatePayloadList = false) {
- gPingData = ping;
- // Render raw ping data.
- let pre = document.getElementById("raw-ping-data");
- pre.textContent = JSON.stringify(gPingData, null, 2);
- // Update the structured data rendering.
- const keysHeader = bundle.GetStringFromName("keysHeader");
- const valuesHeader = bundle.GetStringFromName("valuesHeader");
- // Update the payload list and process lists
- if (updatePayloadList) {
- renderPayloadList(ping);
- renderProcessList(ping, document.getElementById("histograms-processes"));
- renderProcessList(ping, document.getElementById("keyed-histograms-processes"));
- }
- // Show general data.
- GeneralData.render(ping);
- // Show environment data.
- EnvironmentData.render(ping);
- // We only have special rendering code for the payloads from "main" pings.
- // For any other pings we just render the raw JSON payload.
- let isMainPing = (ping.type == "main" || ping.type == "saved-session");
- togglePingSections(isMainPing);
- if (!isMainPing) {
- RawPayload.render(ping);
- return;
- }
- // Show telemetry log.
- TelLog.render(ping);
- // Show slow SQL stats
- SlowSQL.render(ping);
- // Show chrome hang stacks
- ChromeHangs.render(ping);
- // Render Addon details.
- AddonDetails.render(ping);
- // Select payload to render
- let payloadSelect = document.getElementById("choose-payload");
- let payloadOption = payloadSelect.selectedOptions.item(0);
- let payloadIndex = payloadOption.getAttribute("value");
- let payload = ping.payload;
- if (payloadIndex > 0) {
- payload = ping.payload.childPayloads[payloadIndex - 1];
- }
- // Show thread hang stats
- ThreadHangStats.render(payload);
- // Show simple measurements
- let simpleMeasurements = sortStartupMilestones(payload.simpleMeasurements);
- let hasData = Object.keys(simpleMeasurements).length > 0;
- setHasData("simple-measurements-section", hasData);
- let simpleSection = document.getElementById("simple-measurements");
- removeAllChildNodes(simpleSection);
- if (hasData) {
- simpleSection.appendChild(KeyValueTable.render(simpleMeasurements,
- keysHeader, valuesHeader));
- }
- LateWritesSingleton.renderLateWrites(payload.lateWrites);
- // Show basic session info gathered
- hasData = Object.keys(ping.payload.info).length > 0;
- setHasData("session-info-section", hasData);
- let infoSection = document.getElementById("session-info");
- removeAllChildNodes(infoSection);
- if (hasData) {
- infoSection.appendChild(KeyValueTable.render(ping.payload.info,
- keysHeader, valuesHeader));
- }
- // Show scalar data.
- Scalars.render(payload);
- KeyedScalars.render(payload);
- // Show histogram data
- let hgramDiv = document.getElementById("histograms");
- removeAllChildNodes(hgramDiv);
- let histograms = payload.histograms;
- let hgramsSelect = document.getElementById("histograms-processes");
- let hgramsOption = hgramsSelect.selectedOptions.item(0);
- let hgramsProcess = hgramsOption.getAttribute("value");
- if (hgramsProcess &&
- "processes" in ping.payload &&
- hgramsProcess in ping.payload.processes) {
- histograms = ping.payload.processes[hgramsProcess].histograms;
- }
- hasData = Object.keys(histograms).length > 0;
- setHasData("histograms-section", hasData || hgramsSelect.options.length);
- if (hasData) {
- for (let [name, hgram] of Object.entries(histograms)) {
- Histogram.render(hgramDiv, name, hgram, {unpacked: true});
- }
- let filterBox = document.getElementById("histograms-filter");
- filterBox.addEventListener("input", Histogram.histogramFilterChanged, false);
- if (filterBox.value.trim() != "") { // on load, no need to filter if empty
- Histogram.filterHistograms(hgramDiv, filterBox.value);
- }
- setHasData("histograms-section", true);
- }
- // Show keyed histogram data
- let keyedDiv = document.getElementById("keyed-histograms");
- removeAllChildNodes(keyedDiv);
- let keyedHistograms = payload.keyedHistograms;
- let keyedHgramsSelect = document.getElementById("keyed-histograms-processes");
- let keyedHgramsOption = keyedHgramsSelect.selectedOptions.item(0);
- let keyedHgramsProcess = keyedHgramsOption.getAttribute("value");
- if (keyedHgramsProcess &&
- "processes" in ping.payload &&
- keyedHgramsProcess in ping.payload.processes) {
- keyedHistograms = ping.payload.processes[keyedHgramsProcess].keyedHistograms;
- }
- setHasData("keyed-histograms-section", keyedHgramsSelect.options.length);
- if (keyedHistograms) {
- let hasData = false;
- for (let [id, keyed] of Object.entries(keyedHistograms)) {
- if (Object.keys(keyed).length > 0) {
- hasData = true;
- KeyedHistogram.render(keyedDiv, id, keyed, {unpacked: true});
- }
- }
- setHasData("keyed-histograms-section", hasData || keyedHgramsSelect.options.length);
- }
- // Show event data.
- Events.render(payload);
- // Show addon histogram data
- let addonDiv = document.getElementById("addon-histograms");
- removeAllChildNodes(addonDiv);
- let addonHistogramsRendered = false;
- let addonData = payload.addonHistograms;
- if (addonData) {
- for (let [addon, histograms] of Object.entries(addonData)) {
- for (let [name, hgram] of Object.entries(histograms)) {
- addonHistogramsRendered = true;
- Histogram.render(addonDiv, addon + ": " + name, hgram, {unpacked: true});
- }
- }
- }
- setHasData("addon-histograms-section", addonHistogramsRendered);
- }
- window.addEventListener("load", onLoad, false);
|