diff --git a/data/panel.html b/data/panel.html index bff6810..c8a9b13 100644 --- a/data/panel.html +++ b/data/panel.html @@ -10,6 +10,9 @@ font-family: monospace; font-size: 12px; } + h1 { + margin: 0; + } a { color: inherit; } @@ -47,17 +50,20 @@ } .controls { display: table-cell; - width: 150px; + width: 130px; padding: 5px 10px; text-align: right; background: #888; color: white; z-index: 1; } + .childHang .controls { + background: #5555AA; + } .controls .duration { font-size: 12px; } - .controls .time, .controls .uptime { + .controls .time { margin-top: 5px; } .controls .copyButton { @@ -86,10 +92,12 @@

STATUSER SETTINGS

The icon is a red square when the user is active, a blue circle when the user is inactive, and turns into a yellow square when there is a page loading in any tab.

-
+
+
+

-

Statuser can count hangs on the main thread, lags on the event loop, or spikes in input event response time. Changing this setting will reset the hang counter.

+

Statuser can count Gecko thread hangs for the parent process/child process/both, lags on the event loop, or spikes in input event response time. Changing this setting will reset the hang counter.


@@ -108,7 +116,6 @@

STATUSER SETTINGS

On E10S builds, Statuser will only measure main thread hangs.


MOST RECENT HANGS

-

The stack traces displayed here are are Background Hang Reporter pseudo-stacks. Only the 10 most recent stack traces are shown. Stack traces are only captured for hangs of 128ms or greater.

-

Hang uptimes correspond to the X axis in the Gecko Profiler addon timeline.

+

The stack traces displayed here are are Background Hang Reporter pseudo-stacks. Only the 10 most recent stack traces are shown. Stack traces are only captured for hangs of 128ms or greater.

+

Entries with a grey sidebar are parent process hangs, while entries with a blue sidebar are child process hangs. Hang uptimes correspond to the X axis in the Gecko Profiler addon timeline.

\ No newline at end of file diff --git a/data/panel.js b/data/panel.js index ab84cd0..750e9ce 100644 --- a/data/panel.js +++ b/data/panel.js @@ -1,7 +1,15 @@ // emit events on the panel's port for corresponding actions +var countThreadHangsParentOnly = document.getElementById("countThreadHangsParentOnly"); +var countThreadHangsChildOnly = document.getElementById("countThreadHangsChildOnly"); var countThreadHangs = document.getElementById("countThreadHangs"); var countEventLoopLags = document.getElementById("countEventLoopLags"); var countInputEventResponseLags = document.getElementById("countInputEventResponseLags"); +countThreadHangsParentOnly.addEventListener("click", function() { + self.port.emit("mode-changed", "threadHangsParentOnly"); +}); +countThreadHangsChildOnly.addEventListener("click", function() { + self.port.emit("mode-changed", "threadHangsChildOnly"); +}); countThreadHangs.addEventListener("click", function() { self.port.emit("mode-changed", "threadHangs"); }); @@ -32,6 +40,12 @@ self.port.on("show", function(currentSettings) { playSound.checked = currentSettings.playSound; hangThreshold.value = currentSettings.hangThreshold; switch (currentSettings.mode) { + case "threadHangsParentOnly": + document.getElementById("countThreadHangsParentOnly").checked = true; + break; + case "threadHangsChildOnly": + document.getElementById("countThreadHangsChildOnly").checked = true; + break; case "threadHangs": document.getElementById("countThreadHangs").checked = true; break; @@ -53,16 +67,20 @@ self.port.on("warning", function(warningType) { case null: banner.style.display = "none"; break; + case "unavailableChildBHR": + banner.innerHTML = "CHILD PROCESS BACKGROUND HANG REPORTING UNAVAILABLE; CHECK FIREFOX VERSION, ENABLE BHR, AND RESTART FIREFOX"; + banner.style.display = "block"; + break; case "unavailableBHR": - banner.innerHTML = "BACKGROUND HANG REPORTING UNAVAILABLE; ENABLE BHR AND RESTART FIREFOX"; + banner.innerHTML = "BACKGROUND HANG REPORTING UNAVAILABLE; CHECK FIREFOX VERSION, ENABLE BHR, AND RESTART FIREFOX"; banner.style.display = "block"; break; case "unavailableEventLoopLags": - banner.innerHTML = "EVENTLOOP_UI_ACTIVITY_EXP_MS HISTOGRAM UNAVAILABLE; CHECK FIREFOX VERSION"; + banner.innerHTML = "EVENTLOOP_UI_ACTIVITY_EXP_MS HISTOGRAM UNAVAILABLE; CHECK FIREFOX VERSION AND ENABLE TELEMETRY"; banner.style.display = "block"; break; case "unavailableInputEventResponseLags": - banner.innerHTML = "INPUT_EVENT_RESPONSE_MS HISTOGRAM UNAVAILABLE; CHECK FIREFOX VERSION"; + banner.innerHTML = "INPUT_EVENT_RESPONSE_MS HISTOGRAM UNAVAILABLE; CHECK FIREFOX VERSION AND ENABLE TELEMETRY"; banner.style.display = "block"; break; default: @@ -78,6 +96,9 @@ function setHangs(hangs) { hangs.reverse().forEach(hang => { // create an entry for the hang var entry = document.createElement("div"); + if (hang.isChild) { + entry.className = "childHang"; + } var contents = document.createElement("pre"); contents.className = "stack"; contents.appendChild(document.createTextNode(hang.stack)); diff --git a/index.js b/index.js index 93b7811..7ae1648 100644 --- a/index.js +++ b/index.js @@ -3,10 +3,15 @@ var ss = require("sdk/simple-storage"); var clipboard = require("sdk/clipboard"); var windowUtils = require("sdk/window/utils"); +const {Cc, Ci, Cu} = require("chrome"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/TelemetrySession.jsm"); +let gOS = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService); + // load and validate settings var gMode = ss.storage.mode; -if (gMode !== "threadHangs" && gMode !== "eventLoopLags" && gMode !== "inputEventResponseLags") { - gMode = "threadHangs"; +if (["threadHangsParentOnly", "threadHangsChildOnly", "threadHangs", "eventLoopLags", "inputEventResponseLags"].indexOf(gMode) < 0) { + gMode = "threadHangsParentOnly"; } var gPlaySound = ss.storage.playSound; if (typeof gPlaySound !== "boolean") { @@ -17,9 +22,10 @@ if (typeof gHangThreshold !== "number" || gHangThreshold < 1) { gHangThreshold = 126; } -const { setInterval } = require("sdk/timers"); +const { setTimeout } = require("sdk/timers"); const { ActionButton } = require("sdk/ui/button/action"); +// define all the SVG icons const ANIMATE_TEMPLATE = ''; const ANIMATE_ROTATE_SVG = '' + '= gHangThreshold) -function numGeckoHangs() { - var hangs; +// function that retrieves the current child hangs and caches it for a short duration +// caching this is useful since each call has to go through the event queue of both parent and child processes, +// which can be relatively slow if any of the queues are backed up +let cachedPreviousChildHangs = null; +let lastChildHangsRetrievedTime = -Infinity; +const cacheThreadHangsDuration = 300; +function getChildThreadHangs() { + if (Date.now() - lastChildHangsRetrievedTime < cacheThreadHangsDuration) { + // the thread hangs have been retrieved very recently, so we can just return those results again + return Promise.resolve(cachedPreviousChildHangs); + } + lastChildHangsRetrievedTime = Date.now(); + return TelemetrySession.getChildThreadHangs().then((hangs) => { + cachedPreviousChildHangs = hangs; + return hangs; + }); +} + +// returns a promise resolving to the number of Gecko hangs, and the computed minimum threshold for those hangs (which is a value >= gHangThreshold) +function getHangs() { switch(gMode) { + case "threadHangsParentOnly": + return numGeckoThreadHangs(false, true); + case "threadHangsChildOnly": + return numGeckoThreadHangs(true, false); case "threadHangs": - hangs = numGeckoThreadHangs(); - panel.port.emit("warning", hangs === null ? "unavailableBHR" : null); - return hangs + return numGeckoThreadHangs(true, true); case "eventLoopLags": - hangs = numEventLoopLags(); - panel.port.emit("warning", hangs === null ? "unavailableEventLoopLags" : null); - return hangs; + return numEventLoopLags(); case "inputEventResponseLags": - hangs = numInputEventResponseLags(); - panel.port.emit("warning", hangs === null ? "unavailableInputEventResponseLags" : null); - return hangs; + return numInputEventResponseLags(); default: console.warn("Unknown mode: ", gMode); - return {numHangs: null, minBucketLowerBound: 0}; + return Promise.reject(); } } -function numGeckoThreadHangs() { - let geckoThread = Services.telemetry.threadHangStats.find(thread => - thread.name == "Gecko" - ); - if (!geckoThread || !geckoThread.activity.counts) { - console.warn("Lolwhut? No Gecko thread? No hangs?"); - return {numHangs: null, minBucketLowerBound: 0}; +function numGeckoThreadHangs(includeChildHangs, includeParentHangs) { + if (includeChildHangs && !TelemetrySession.getChildThreadHangs) { + panel.port.emit("warning", "unavailableChildBHR"); + return Promise.reject(); } - // see the NOTE in mostRecentHangs() for caveats when using the activity.counts histogram - // to summarize, the ranges are the inclusive upper bound of the histogram rather than the inclusive lower bound - let numHangs = 0; - let minBucketLowerBound = Infinity; - geckoThread.activity.counts.forEach((count, i) => { - var lowerBound = geckoThread.activity.ranges[i - 1] + 1; - if (lowerBound >= gHangThreshold) { - numHangs += count; - minBucketLowerBound = Math.min(minBucketLowerBound, lowerBound); + + let counts = []; + let ranges = []; + if (includeParentHangs) { + geckoThread = Services.telemetry.threadHangStats.find(thread => thread.name == "Gecko"); + if (!geckoThread || !geckoThread.activity.counts) { + panel.port.emit("warning", "unavailableBHR"); + return Promise.reject(); + } + counts = geckoThread.activity.counts.slice(0); + ranges = geckoThread.activity.ranges; + } + + return new Promise((resolve) => { + if (includeChildHangs) { + getChildThreadHangs().then((hangs) => { + // accumulate all of the counts in the child processes with the counts in the parent process + hangs.forEach((threadHangStats) => { + let childGeckoThread = threadHangStats.find(thread => thread.name == "Gecko_Child"); + if (childGeckoThread && childGeckoThread.activity.counts) { + childGeckoThread.activity.counts.forEach((count, i) => { counts[i] = (counts[i] || 0) + count; }); + if (ranges.length === 0) { + ranges = childGeckoThread.activity.ranges; + } + } + }); + resolve(); + }); + } else { // Just use the parent process stats + resolve(); } + }).then(() => { + // see the NOTE in mostRecentHangs() for caveats when using the activity.counts histogram + // to summarize, the ranges are the inclusive upper bound of the histogram rather than the inclusive lower bound + let numHangs = 0; + let minBucketLowerBound = Infinity; + counts.forEach((count, i) => { + var lowerBound = ranges[i - 1] + 1; + if (lowerBound >= gHangThreshold) { + numHangs += count; + minBucketLowerBound = Math.min(minBucketLowerBound, lowerBound); + } + }); + return {numHangs: numHangs, minBucketLowerBound: minBucketLowerBound}; }); - return {numHangs: numHangs, minBucketLowerBound: minBucketLowerBound}; } function numEventLoopLags() { try { var snapshot = Services.telemetry.getHistogramById("EVENTLOOP_UI_ACTIVITY_EXP_MS").snapshot(); } catch (e) { // histogram doesn't exist, the Firefox version is likely older than 45.0a1 - return {numHangs: null, minBucketLowerBound: 0}; + panel.port.emit("warning", "unavailableEventLoopLags"); + return Promise.reject(); } let numHangs = 0; let minBucketLowerBound = Infinity; @@ -200,24 +248,134 @@ function numEventLoopLags() { minBucketLowerBound = Math.min(minBucketLowerBound, snapshot.ranges[i]); } } - return {numHangs: numHangs, minBucketLowerBound: minBucketLowerBound}; + return Promise.resolve({numHangs: numHangs, minBucketLowerBound: minBucketLowerBound}); } function numInputEventResponseLags() { try { var snapshot = Services.telemetry.getHistogramById("INPUT_EVENT_RESPONSE_MS").snapshot(); } catch (e) { // histogram doesn't exist, the Firefox version is likely older than 46.0a1 - return {numHangs: null, minBucketLowerBound: 0}; + panel.port.emit("warning", "unavailableInputEventResponseLags"); + return Promise.reject(); } let numHangs = 0; let minBucketLowerBound = Infinity; for (let i = 0; i < snapshot.ranges.length; ++i) { if (snapshot.ranges[i] > gHangThreshold) { - result += snapshot.counts[i]; + numHangs += snapshot.counts[i]; minBucketLowerBound = Math.min(minBucketLowerBound, snapshot.ranges[i]); } } - return {numHangs: numHangs, minBucketLowerBound: minBucketLowerBound}; + return Promise.resolve({numHangs: numHangs, minBucketLowerBound: minBucketLowerBound}); +} + +// returns a promise resolving to an array of the most recent BHR hangs +let previousCountsMap = {}; // this is a mapping from stack traces (as strings) to corresponding histogram counts +let cachedRecentHangs = []; +let lastMostRecentHangsTime = getUptime(); +function mostRecentHangs() { + let includeParentHangs = false; + let includeChildHangs = false; + switch(gMode) { + case "threadHangsParentOnly": + includeParentHangs = true; + break; + case "threadHangsChildOnly": + includeChildHangs = true; + break; + case "threadHangs": + includeParentHangs = includeChildHangs = true; + break; + } + + if (includeChildHangs && TelemetrySession.getChildThreadHangs === undefined) { + panel.port.emit("warning", "unavailableChildBHR"); + return Promise.reject(); + } + + let parentHangs = []; + if (includeParentHangs) { + let geckoThread = Services.telemetry.threadHangStats.find(thread => thread.name == "Gecko"); + if (!geckoThread || !geckoThread.hangs) { + panel.port.emit("warning", "unavailableBHR"); + return Promise.reject(); + } + parentHangs = geckoThread.hangs; + } + + return new Promise((resolve) => { + if (includeChildHangs) { + getChildThreadHangs().then((hangs) => { + // accumulate all of the counts in the child processes with the counts in the parent process + let childHangEntries = []; + hangs.forEach((threadHangStats) => { + let childGeckoThread = threadHangStats.find(thread => thread.name == "Gecko_Child"); + if (childGeckoThread && childGeckoThread.activity.counts) { + childHangEntries = childHangEntries.concat(childGeckoThread.hangs); + } + }); + resolve([parentHangs, childHangEntries]); + }); + } else { // Just use the parent process stats + resolve([parentHangs, []]); + } + }).then(hangInfo => { + [parentHangEntries, childHangEntries] = hangInfo; + let timestamp = (new Date()).getTime(); // note that this timestamp will only be as accurate as the interval at which this function is called + let uptime = getUptime(); // this value matches the X axis in the timeline for the Gecko Profiler addon + + // diff the current hangs with the previous hangs to figure out what changed in this call, if anything + // hangs list will only ever grow: https://dxr.mozilla.org/mozilla-central/source/xpcom/threads/BackgroundHangMonitor.cpp#440 + // therefore, we only need to check current stacks against previous stacks - there is no need for a 2 way diff + // hangs are identified by their stack traces: https://dxr.mozilla.org/mozilla-central/source/toolkit/components/telemetry/Telemetry.cpp#4316 + function diffHangEntry(hangEntry, isChild) { + var stack = hangEntry.stack.slice(0).reverse().join("\n"); + var ranges = hangEntry.histogram.ranges.concat([Infinity]); + var counts = hangEntry.histogram.counts; + var previousCounts = previousCountsMap.hasOwnProperty(stack) ? previousCountsMap[stack] : []; + + // diff this hang histogram with the previous hang histogram + counts.forEach((count, i) => { + var previousCount = previousCounts[i] || 0; + /* + NOTE: when you access the thread hangs, the ranges are actually the inclusive upper bounds of the buckets rather than the inclusive lower bound like other histograms. + Basically, when we access the buckets of a TimeHistogram in JS, it has a 0 prepended to the ranges; in C++, the indices behave as all other histograms do. + + For example, bucket 7 actually represents hangs of duration 64ms to 127ms, inclusive. For most other exponential histograms, this would be 128ms to 255ms. + + References: + * mozilla::Telemetry::CreateJSTimeHistogram - http://mxr.mozilla.org/mozilla-central/source/toolkit/components/telemetry/Telemetry.cpp#2947 + * mozilla::Telemetry::TimeHistogram - http://mxr.mozilla.org/mozilla-central/source/toolkit/components/telemetry/ThreadHangStats.h#25 + */ + while (count > previousCount) { // each additional count here is a new hang with this stack and a duration in this bucket's range + let lowerBound = ranges[i - 1] + 1; + if (lowerBound >= gHangThreshold) { + cachedRecentHangs.push({ + stack: stack, lowerBound: lowerBound, upperBound: ranges[i], + timestamp: timestamp, uptime: uptime, previousUptime: lastMostRecentHangsTime, + isChild: isChild, + }); + if (cachedRecentHangs.length > 10) { // only keep the last 10 items + cachedRecentHangs.shift(); + } + } + count --; + } + }); + + // the hang entry is not mutated when new instances of this hang come in + // since we aren't using this entry in the previous hangs anymore, we can just set it in the previous hangs + previousCountsMap[stack] = counts; + } + + // diff the hang entries with their previous counts + // this mutates cachedRecentHangs so that it contains the differences + parentHangEntries.forEach(entry => { diffHangEntry(entry, false); }); + childHangEntries.forEach(entry => { diffHangEntry(entry, true); }); + + lastMostRecentHangsTime = uptime; + return cachedRecentHangs; + }); } var soundPlayerPage = require("sdk/page-worker").Page({ @@ -238,68 +396,11 @@ function getUptime() { } } -// Returns an array of the most recent BHR hangs -var previousCountsMap = {}; // this is a mapping from stack traces (as strings) to corresponding histogram counts -var recentHangs = []; -let lastMostRecentHangsTime = getUptime(); -function mostRecentHangs() { - let geckoThread = Services.telemetry.threadHangStats.find(thread => - thread.name == "Gecko" - ); - if (!geckoThread) { - console.warn("Uh oh, there doesn't seem to be a thread with name \"Gecko\"!"); - return []; - } - - var timestamp = (new Date()).getTime(); // note that this timestamp will only be as accurate as the interval at which this function is called - var uptime = getUptime(); // this value matches the X axis in the timeline for the Gecko Profiler addon - - // diff the current hangs with the previous hangs to figure out what changed in this call, if anything - // hangs list will only ever grow: https://dxr.mozilla.org/mozilla-central/source/xpcom/threads/BackgroundHangMonitor.cpp#440 - // therefore, we only need to check current stacks against previous stacks - there is no need for a 2 way diff - // hangs are identified by their stack traces: https://dxr.mozilla.org/mozilla-central/source/toolkit/components/telemetry/Telemetry.cpp#4316 - geckoThread.hangs.forEach(hangEntry => { - var stack = hangEntry.stack.slice(0).reverse().join("\n"); - var ranges = hangEntry.histogram.ranges.concat([Infinity]); - var counts = hangEntry.histogram.counts; - var previousCounts = previousCountsMap.hasOwnProperty(stack) ? previousCountsMap[stack] : []; - - // diff this hang histogram with the previous hang histogram - counts.forEach((count, i) => { - var previousCount = previousCounts[i] || 0; - /* - NOTE: when you access the thread hangs, the ranges are actually the inclusive upper bounds of the buckets rather than the inclusive lower bound like other histograms. - Basically, when we access the buckets of a TimeHistogram in JS, it has a 0 prepended to the ranges; in C++, the indices behave as all other histograms do. - - For example, bucket 7 actually represents hangs of duration 64ms to 127ms, inclusive. For most other exponential histograms, this would be 128ms to 255ms. - - References: - * mozilla::Telemetry::CreateJSTimeHistogram - http://mxr.mozilla.org/mozilla-central/source/toolkit/components/telemetry/Telemetry.cpp#2947 - * mozilla::Telemetry::TimeHistogram - http://mxr.mozilla.org/mozilla-central/source/toolkit/components/telemetry/ThreadHangStats.h#25 - */ - while (count > previousCount) { // each additional count here is a new hang with this stack and a duration in this bucket's range - let lowerBound = ranges[i - 1] + 1; - if (lowerBound >= gHangThreshold) { - recentHangs.push({stack: stack, lowerBound: lowerBound, upperBound: ranges[i], timestamp: timestamp, uptime: uptime, previousUptime: lastMostRecentHangsTime}); - if (recentHangs.length > 10) { // only keep the last 10 items - recentHangs.shift(); - } - } - count --; - } - }); - - // the hang entry is not mutated when new instances of this hang come in - // since we aren't using this entry in the previous hangs anymore, we can just set it in the previous hangs - previousCountsMap[stack] = counts; - }); - lastMostRecentHangsTime = uptime; - return recentHangs; -} - -const BADGE_COLOURS = ["red", "blue", "brown", "black"]; +let computedThreshold = 0; let numHangsObserved = 0; let prevNumHangs = null; +let baseNumHangs = 0; // the number of hangs at the time the counter was last reset +const BADGE_COLOURS = ["red", "blue", "brown", "black"]; function updateBadge() { if (numHangs === null) { button.badge = "?" @@ -317,43 +418,77 @@ function updateBadge() { } } -// reset the current hang stacks so we only show the new ones coming in -mostRecentHangs(); -recentHangs = []; - -const CHECK_FOR_HANG_INTERVAL = 400; // in millis -let { numHangs: numHangs, minBucketLowerBound: computedThreshold } = numGeckoHangs(); // note: this will be null if the hang counter is not available -let baseNumHangs = 0; // the number of hangs at the time the counter was last reset -setInterval(() => { - let { numHangs: hangCount, minBucketLowerBound: lower } = numGeckoHangs(); - if (hangCount !== numHangs) { - numHangs = hangCount; - updateBadge(); - let hangs = mostRecentHangs(); - panel.port.emit("set-hangs", hangs); - if (hangs.length > 0) { - button.label = "Most recent hang stack:\n\n" + hangs[hangs.length - 1].stack; - } else { - button.label = "No recent hang stacks."; - } - //exports.observe(undefined, "thread-hang"); - } - if (lower !== computedThreshold) { // update the computed threshold - computedThreshold = lower; - panel.port.emit("set-computed-threshold", computedThreshold); - } -}, CHECK_FOR_HANG_INTERVAL); -clearCount(); - function clearCount() { - baseNumHangs = numHangs; + baseNumHangs = prevNumHangs = numHangs; numHangsObserved = 0; + computedThreshold = 0; + cachedRecentHangs = []; // empty out the list of hangs updateBadge(); panel.port.emit("set-computed-threshold", computedThreshold); panel.port.emit("set-hangs", []); // clear the panel's list of hangs - recentHangs = []; // empty out the list of hangs } +const CHECK_FOR_HANG_INTERVAL = 400; // in millis +function update() { + getHangs().then(({numHangs: hangCount, minBucketLowerBound: lower}) => { + if (lower !== computedThreshold) { // update the computed threshold + computedThreshold = lower; + panel.port.emit("set-computed-threshold", computedThreshold); + } + if (hangCount > numHangs) { // new hangs detected + mostRecentHangs().then((recentHangs) => { + numHangs = hangCount; + if (shouldClearHangs) { + clearCount(); + recentHangs = []; // clear the current list of hangs, which we received before clearing the counts + shouldClearHangs = false; + } + updateBadge(); + + // update the button label + if (recentHangs.length > 0) { + // show the hang stack in the button tooltip + button.label = "Most recent hang stack:\n\n" + recentHangs[recentHangs.length - 1].stack; + } else { + button.label = "No recent hang stacks."; + } + + panel.port.emit("warning", null); + panel.port.emit("set-hangs", recentHangs); + setTimeout(update, CHECK_FOR_HANG_INTERVAL); + }, () => { // failed to retrieve hangs + numHangs = hangCount; + if (shouldClearHangs) { + clearCount(); + shouldClearHangs = false; + } + updateBadge(); + + // update the button label + button.label = "Could not retrieve hang stacks."; + + panel.port.emit("set-hangs", []); + setTimeout(update, CHECK_FOR_HANG_INTERVAL); + }); + } else { // no new hangs + if (shouldClearHangs) { + clearCount(); + updateBadge(); + shouldClearHangs = false; + } + setTimeout(update, CHECK_FOR_HANG_INTERVAL); + } + }, () => { + if (numHangs !== null) { + computedThreshold = 0; + panel.port.emit("set-computed-threshold", 0); + numHangs = null; + updateBadge(); + } + }); +} +update(); + /* Enable this rAF loop to verify that the hangs reported are roughly equal * to the number of hangs observed from script. In Nightly 45, they were. var prevFrameTime = Cu.now();