зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
4128a5f6ec
Коммит
3d718557fc
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
Загрузка…
Ссылка в новой задаче