Bug 1580469 - Create end to end profile capture tests; r=julienw

This patch creates the first full mochitest that exercises the profiler
popup's mechanisms of capturing profiles. The test tries to use user-focused
mechanisms–clicking buttons to fully capture a profile.

In addition, it fixes two leaks that were uncovered by the leaktest check.
The first has to do with the iframe src creating an about:blank page when
set to an empty string. The next was the Services.obs.addObserver call
in the perfFrontInterface not being removed when the page was unloaded.

Differential Revision: https://phabricator.services.mozilla.com/D45530

--HG--
extra : source : f44e632769d75b428d0a84a3274bdf45095b91fc
This commit is contained in:
Greg Tatum 2019-10-04 18:23:48 +00:00
Родитель 4128a5f6ec
Коммит 3d718557fc
10 изменённых файлов: 379 добавлений и 30 удалений

Просмотреть файл

@ -638,9 +638,7 @@
<vbox id="PanelUI-developerItems" class="panel-subview-body"/>
</panelview>
<panelview id="PanelUI-profiler" flex="1">
<iframe id="PanelUI-profilerIframe" className="PanelUI-developer-iframe" />
</panelview>
<panelview id="PanelUI-profiler" flex="1"/>
<panelview id="PanelUI-characterEncodingView" flex="1">
<vbox class="panel-subview-body">

Просмотреть файл

@ -17,7 +17,9 @@ const TRANSFER_EVENT = "devtools:perf-html-transfer-profile";
const SYMBOL_TABLE_REQUEST_EVENT = "devtools:perf-html-request-symbol-table";
const SYMBOL_TABLE_RESPONSE_EVENT = "devtools:perf-html-reply-symbol-table";
const UI_BASE_URL_PREF = "devtools.performance.recording.ui-base-url";
const UI_BASE_URL_PATH_PREF = "devtools.performance.recording.ui-base-url-path";
const UI_BASE_URL_DEFAULT = "https://profiler.firefox.com";
const UI_BASE_URL_PATH_DEFAULT = "/from-addon";
const OBJDIRS_PREF = "devtools.performance.recording.objdirs";
/**
@ -49,11 +51,18 @@ function receiveProfile(profile, getSymbolTableCallback) {
const browser = win.gBrowser;
Services.focus.activeWindow = win;
// Allow the user to point to something other than profiler.firefox.com.
const baseUrl = Services.prefs.getStringPref(
UI_BASE_URL_PREF,
UI_BASE_URL_DEFAULT
);
const tab = browser.addWebTab(`${baseUrl}/from-addon`, {
// Allow tests to override the path.
const baseUrlPath = Services.prefs.getStringPref(
UI_BASE_URL_PATH_PREF,
UI_BASE_URL_PATH_DEFAULT
);
const tab = browser.addWebTab(`${baseUrl}${baseUrlPath}`, {
triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({
userContextId: browser.contentPrincipal.userContextId,
}),

Просмотреть файл

@ -16,6 +16,7 @@ DevToolsModules(
'utils.js',
)
BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
MOCHITEST_CHROME_MANIFESTS += ['test/chrome/chrome.ini']
XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell/xpcshell.ini']

Просмотреть файл

@ -58,18 +58,16 @@ document.addEventListener("DOMContentLoaded", () => {
/**
* Initialize the panel by creating a redux store, and render the root component.
*
* @param perfFront - The Perf actor's front. Used to start and stop recordings.
* @param preferenceFront - Used to get the recording preferences from the device.
*/
async function gInit(perfFront, preferenceFront) {
async function gInit() {
const store = createStore(reducers);
const perfFrontInterface = new ActorReadyGeckoProfilerInterface();
// Do some initialization, especially with privileged things that are part of the
// the browser.
store.dispatch(
actions.initializeStore({
perfFront: new ActorReadyGeckoProfilerInterface(),
perfFront: perfFrontInterface,
receiveProfile,
// Pull the default recording settings from the reducer, and update them according
// to what's in the browser's preferences.
@ -93,6 +91,12 @@ async function gInit(perfFront, preferenceFront) {
document.querySelector("#root")
);
window.addEventListener("unload", function() {
// The perf front interface needs to be unloaded in order to remove event handlers.
// Not doing so leads to leaks.
perfFrontInterface.destroy();
});
resizeWindow();
}

Просмотреть файл

@ -65,22 +65,6 @@ function toggle(document) {
}
}
/**
* This is a utility function to get the iframe from an event.
* @param {Object} The event fired by the CustomizableUI interface, contains a target.
*/
function getIframeFromEvent(event) {
const panelview = event.target;
const document = panelview.ownerDocument;
// Create the iframe, and append it.
const iframe = document.getElementById("PanelUI-profilerIframe");
if (!iframe) {
throw new Error("Unable to select the PanelUI-profilerIframe.");
}
return iframe;
}
// This function takes the button element, and returns a function that's used to
// update the profiler button whenever the profiler activation status changed.
const updateButtonColorForElement = buttonElement => () => {
@ -109,10 +93,18 @@ function initialize() {
viewId: "PanelUI-profiler",
tooltiptext: "profiler-button.tooltiptext",
onViewShowing: event => {
const iframe = getIframeFromEvent(event);
const panelview = event.target;
const document = panelview.ownerDocument;
// Create an iframe and append it to the panelview.
const iframe = document.createXULElement("iframe");
iframe.id = "PanelUI-profilerIframe";
iframe.className = "PanelUI-developer-iframe";
iframe.src =
"chrome://devtools/content/performance-new/popup/popup.xhtml";
panelview.appendChild(iframe);
// Provide a mechanism for the iframe to close the popup.
iframe.contentWindow.gClosePopup = () => {
CustomizableUI.hidePanelForNode(iframe);
@ -138,10 +130,16 @@ function initialize() {
);
},
onViewHiding(event) {
const iframe = getIframeFromEvent(event);
// Unset the iframe src so that when the popup DOM element is moved, the popup's
// contents aren't re-initialized.
iframe.src = "";
const document = event.target.ownerDocument;
// Create the iframe, and append it.
const iframe = document.getElementById("PanelUI-profilerIframe");
if (!iframe) {
throw new Error("Unable to select the PanelUI-profilerIframe.");
}
// Remove the iframe so it doesn't leak.
iframe.remove();
},
onBeforeCreated: document => {
setMenuItemChecked(document, true);

Просмотреть файл

@ -0,0 +1,9 @@
/* 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';
module.exports = {
// Extend from the shared list of defined globals for mochitests.
'extends': '../../../../.eslintrc.mochitests.js'
};

Просмотреть файл

@ -0,0 +1,8 @@
[DEFAULT]
tags = devtools devtools-performance
subsuite = devtools
support-files =
head.js
fake-frontend.html
[browser_popup-end-to-end-click.js]

Просмотреть файл

@ -0,0 +1,40 @@
/* 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";
add_task(async function test() {
info(
"Test that the profiler pop-up works end to end with profile recording and " +
"capture using the mouse and hitting buttons."
);
await setProfilerFrontendUrl(
"http://example.com/browser/devtools/client/performance-new/test/browser/fake-frontend.html"
);
await makeSureProfilerPopupIsEnabled();
toggleOpenProfilerPopup();
{
const button = await getElementFromPopupByText("Start recording");
info("Click the button to start recording.");
button.click();
}
{
const button = await getElementFromPopupByText("Capture recording");
info("Click the button to capture the recording.");
button.click();
}
info(
"If the profiler successfully injects a profile into the page, then the " +
"fake frontend will rename the title of the page."
);
await checkTabLoadedProfile({
initialTitle: "Waiting on the profile",
successTitle: "Profile received",
errorTitle: "Error",
});
});

Просмотреть файл

@ -0,0 +1,67 @@
<!DOCTYPE html>
<!-- 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/. -->
<html>
<head>
<meta charset="utf-8"/>
<title></title>
</head>
<body>
<script>
"use strict";
// This file is used to test the injection of performance profiles into a front-end,
// specifically the mechanism used to inject into profiler.firefox.com. Rather
// than using some kind of complicated message passing scheme to talk to the test
// harness, modify the title of the page. The tests can easily read the window
// title to see if things worked as expected.
// The following are the titles used to communicate the page's state to the tests.
// Keep these in sync with any tests that read them.
const initialTitle = "Waiting on the profile";
const successTitle = "Profile received";
const errorTitle = "Error"
document.title = initialTitle;
// The following gets called by the frame script, and provides an API to
// receive the profile.
window.connectToGeckoProfiler = async (geckoProfiler) => {
try {
// Get the profile.
const profile = await geckoProfiler.getProfile();
// Check that the profile is somewhat reasonable. It should be a gzipped
// profile, so we can only lightly check some properties about it, and check
// that it is an ArrayBuffer.
//
// After the check, modify the title of the document, so the tab title gets
// updated. This is an easy way to pass a message to the test script.
if (
profile &&
typeof profile === 'object' &&
profile instanceof ArrayBuffer
) {
// The profile looks good!
document.title = successTitle;
} else {
// The profile doesn't look right, surface the error to the terminal.
dump('The gecko profile was malformed in fake-frontend.html\n');
dump(`Profile: ${JSON.stringify(profile)}\n`);
// Also to the web console.
console.error(profile);
// Report the error to the tab title.
document.title = errorTitle;
}
} catch (error) {
// Catch any error and notify the test.
document.title = errorTitle;
dump('An error was caught in fake-frontend.html\n');
dump(`${error}\n`);
}
}
</script>
</body>
</html>

Просмотреть файл

@ -0,0 +1,215 @@
/* 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";
/**
* Wait for a single requestAnimationFrame tick.
*/
function tick() {
return new Promise(resolve => requestAnimationFrame(resolve));
}
/**
* It can be confusing when waiting for something asynchronously. This function
* logs out a message periodically (every 1 second) in order to create helpful
* log messages.
* @param {string} message
* @returns {Function}
*/
function createPeriodicLogger() {
let startTime = Date.now();
let lastCount = 0;
let lastMessage = null;
return message => {
if (lastMessage === message) {
// The messages are the same, check if we should log them.
const now = Date.now();
const count = Math.floor((now - startTime) / 1000);
if (count !== lastCount) {
info(
`${message} (After ${count} ${count === 1 ? "second" : "seconds"})`
);
lastCount = count;
}
} else {
// The messages are different, log them now, and reset the waiting time.
info(message);
startTime = Date.now();
lastCount = 0;
lastMessage = message;
}
};
}
/**
* Wait until a condition is fullfilled.
* @param {Function} condition
* @param {string?} logMessage
* @return The truthy result of the condition.
*/
async function waitUntil(condition, message) {
const logPeriodically = createPeriodicLogger();
// Loop through the condition.
while (true) {
if (message) {
logPeriodically(message);
}
const result = condition();
if (result) {
return result;
}
await tick();
}
}
/**
* This function will select a node from the XPath.
* @returns {HTMLElement?}
*/
function getElementByXPath(document, path) {
return document.evaluate(
path,
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
).singleNodeValue;
}
/**
* This function looks inside of the profiler popup's iframe for some element
* that contains some text. It runs in a loop every requestAnimationFrame until
* it finds an element. If it doesn't find the element it throws an error.
* It also doesn't assume the popup will be visible yet, as this popup showing
* is an async event.
* @param {string} text
* @param {number} maxTicks (optional)
*/
async function getElementFromPopupByText(text) {
const xpath = `//*[contains(text(), '${text}')]`;
return waitUntil(() => {
const iframe = document.getElementById("PanelUI-profilerIframe");
if (iframe) {
return getElementByXPath(iframe.contentDocument, xpath);
}
return null;
}, `Trying to find the element with the text "${text}".`);
}
/**
* Make sure the profiler popup is enabled.
*/
async function makeSureProfilerPopupIsEnabled() {
info("Make sure the profiler popup is enabled.");
info("> Load the profiler menu button.");
const { ProfilerMenuButton } = ChromeUtils.import(
"resource://devtools/client/performance-new/popup/menu-button.jsm"
);
if (!ProfilerMenuButton.isEnabled()) {
info("> The menu button is not enabled, turn it on.");
ProfilerMenuButton.toggle(document);
await waitUntil(
() => gBrowser.ownerDocument.getElementById("profiler-button"),
"> Waiting until the profiler button is added to the browser."
);
await SimpleTest.promiseFocus(gBrowser.ownerGlobal);
registerCleanupFunction(() => {
info(
"Clean up after the test by disabling the profiler popup menu button."
);
if (!ProfilerMenuButton.isEnabled()) {
throw new Error(
"Expected the profiler popup to still be enabled during the test cleanup."
);
}
ProfilerMenuButton.toggle(document);
});
} else {
info("> The menu button was already enabled.");
}
}
/**
* This function toggles the profiler menu button, and then uses user gestures
* to click it open.
*/
function toggleOpenProfilerPopup() {
info("Toggle open the profiler popup.");
info("> Find the profiler menu button.");
const profilerButton = document.getElementById("profiler-button");
if (!profilerButton) {
throw new Error("Could not find the profiler button in the menu.");
}
info("> Trigger a click on the profiler menu button.");
profilerButton.click();
}
/**
* This function overwrites the default profiler.firefox.com URL for tests. This
* ensures that the tests do not attempt to access external URLs.
* @param {string} url
* @returns {Promise}
*/
function setProfilerFrontendUrl(url) {
info("Setting the profiler URL to the fake frontend.");
return SpecialPowers.pushPrefEnv({
set: [
// Make sure observer and testing function run in the same process
["devtools.performance.recording.ui-base-url", url],
["devtools.performance.recording.ui-base-url-path", ""],
],
});
}
/**
* This function checks the document title of a tab to see what the state is.
* This creates a simple messaging mechanism between the content page and the
* test harness. This function runs in a loop every requestAnimationFrame, and
* checks for a sucess title. In addition, an "initialTitle" and "errorTitle"
* can be specified for nicer test output.
* @param {Object}
* {
* initialTitle: string,
* successTitle: string,
* errorTitle: string
* }
*/
async function checkTabLoadedProfile({
initialTitle,
successTitle,
errorTitle,
}) {
const logPeriodically = createPeriodicLogger();
info("Attempting to see if the selected tab can receive a profile.");
return waitUntil(() => {
switch (gBrowser.selectedTab.textContent) {
case initialTitle:
logPeriodically(`> Waiting for the profile to be received.`);
return false;
case successTitle:
ok(true, "The profile was successfully injected to the page");
BrowserTestUtils.removeTab(gBrowser.selectedTab);
return true;
case errorTitle:
throw new Error(
"The fake frontend indicated that there was an error injecting the profile."
);
default:
logPeriodically(`> Waiting for the fake frontend tab to be loaded.`);
return false;
}
});
}