Skip to content

Commit

Permalink
Bug 1861445 - Add the runtime.onPerformanceWarning WebExtension event…
Browse files Browse the repository at this point in the history
… r=zombie,robwu

When an extension's content script is very slow and causes a webpage
to hang noticeably, a warning banner is displayed to the user. It
would be useful to also notify the extension developer when that
happens, so that they can address the issue.

Let's add a new event runtime.onPerformanceWarning that can be
dispatched when the browser needs to warn an extension of runtime
performance issues. For now, let's just dispatch that event when the
slow extension warning banner is displayed to the user.

See also w3c/webextensions#456

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

UltraBlame original commit: 9d33a3994005a05a8fbc4ad307e4bd6fb4ab2a63
  • Loading branch information
marco-c committed Feb 1, 2024
1 parent 1234fe7 commit 51c2cb8
Show file tree
Hide file tree
Showing 5 changed files with 240 additions and 0 deletions.
3 changes: 3 additions & 0 deletions browser/components/extensions/test/browser/browser.toml
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,8 @@ run-if = ["crashreporter"]

["browser_ext_request_permissions.js"]

["browser_ext_runtime_onPerformanceWarning.js"]

["browser_ext_runtime_openOptionsPage.js"]

["browser_ext_runtime_openOptionsPage_uninstall.js"]
Expand Down Expand Up @@ -455,6 +457,7 @@ https_first_disabled = true
skip-if = [
"debug",
"asan",
"tsan" # Bug 1874317
]

["browser_ext_tab_runtimeConnect.js"]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@


"use strict";

const {
Management: {
global: { tabTracker },
},
} = ChromeUtils.importESModule("resource://gre/modules/Extension.sys.mjs");

const {
ExtensionUtils: { promiseObserved },
} = ChromeUtils.importESModule("resource://gre/modules/ExtensionUtils.sys.mjs");

class TestHangReport {
constructor(addonId, scriptBrowser) {
this.addonId = addonId;
this.scriptBrowser = scriptBrowser;
this.QueryInterface = ChromeUtils.generateQI(["nsIHangReport"]);
}

userCanceled() {}
terminateScript() {}

isReportForBrowserOrChildren(frameLoader) {
return (
!this.scriptBrowser || this.scriptBrowser.frameLoader === frameLoader
);
}
}

function dispatchHangReport(extensionId, scriptBrowser) {
const hangObserved = promiseObserved("process-hang-report");

Services.obs.notifyObservers(
new TestHangReport(extensionId, scriptBrowser),
"process-hang-report"
);

return hangObserved;
}

function background() {
let onPerformanceWarningDetails = null;

browser.runtime.onPerformanceWarning.addListener(details => {
onPerformanceWarningDetails = details;
});

browser.test.onMessage.addListener(message => {
if (message === "get-on-performance-warning-details") {
browser.test.sendMessage(
"on-performance-warning-details",
onPerformanceWarningDetails
);
onPerformanceWarningDetails = null;
}
});
}

async function expectOnPerformanceWarningDetails(
extension,
expectedOnPerformanceWarningDetails
) {
extension.sendMessage("get-on-performance-warning-details");

let actualOnPerformanceWarningDetails = await extension.awaitMessage(
"on-performance-warning-details"
);
Assert.deepEqual(
actualOnPerformanceWarningDetails,
expectedOnPerformanceWarningDetails,
expectedOnPerformanceWarningDetails
? "runtime.onPerformanceWarning fired with correct details"
: "runtime.onPerformanceWarning didn't fire"
);
}

add_task(async function test_should_fire_on_process_hang_report() {
const description =
"Slow extension content script caused a page hang, user was warned.";

const extension = ExtensionTestUtils.loadExtension({ background });
await extension.startup();

const notificationPromise = BrowserTestUtils.waitForGlobalNotificationBar(
window,
"process-hang"
);

const tabs = await Promise.all([
BrowserTestUtils.openNewForegroundTab(gBrowser),
BrowserTestUtils.openNewForegroundTab(gBrowser),
]);


await expectOnPerformanceWarningDetails(extension, null);



await dispatchHangReport(extension.id, tabs[0].linkedBrowser);
await expectOnPerformanceWarningDetails(extension, {
category: "content_script",
severity: "high",
description,
tabId: tabTracker.getId(tabs[0]),
});


await dispatchHangReport("wrong-addon-id", tabs[0].linkedBrowser);
await expectOnPerformanceWarningDetails(extension, null);


await dispatchHangReport(null, tabs[0].linkedBrowser);
await expectOnPerformanceWarningDetails(extension, null);



await dispatchHangReport(extension.id, tabs[1].linkedBrowser);
await expectOnPerformanceWarningDetails(extension, {
category: "content_script",
severity: "high",
description,
tabId: tabTracker.getId(tabs[1]),
});



await dispatchHangReport(extension.id, null);
await expectOnPerformanceWarningDetails(extension, {
category: "content_script",
severity: "high",
description,
});

await Promise.all(tabs.map(BrowserTestUtils.removeTab));
await extension.unload();



const notification = await notificationPromise;
Assert.ok(notification.isConnected, "Notification still present");
notification.buttonContainer.querySelector("[label='Stop']").click();
});
48 changes: 48 additions & 0 deletions toolkit/components/extensions/parent/ext-runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

"use strict";





var { ExtensionParent } = ChromeUtils.importESModule(
"resource://gre/modules/ExtensionParent.sys.mjs"
);
Expand Down Expand Up @@ -83,6 +87,43 @@ this.runtime = class extends ExtensionAPIPersistent {
},
};
},
onPerformanceWarning({ fire }) {
let { extension } = this;

let observer = (subject, topic) => {
let report = subject.QueryInterface(Ci.nsIHangReport);

if (report?.addonId !== extension.id) {
return;
}

const performanceWarningEventDetails = {
category: "content_script",
severity: "high",
description:
"Slow extension content script caused a page hang, user was warned.",
};

let scriptBrowser = report.scriptBrowser;
let nativeTab =
scriptBrowser?.ownerGlobal.gBrowser?.getTabForBrowser(scriptBrowser);
if (nativeTab) {
performanceWarningEventDetails.tabId = tabTracker.getId(nativeTab);
}

fire.async(performanceWarningEventDetails);
};

Services.obs.addObserver(observer, "process-hang-report");
return {
unregister: () => {
Services.obs.removeObserver(observer, "process-hang-report");
},
convert(_fire, context) {
fire = _fire;
},
};
},
};

getAPI(context) {
Expand Down Expand Up @@ -171,6 +212,13 @@ this.runtime = class extends ExtensionAPIPersistent {
},
}).api(),

onPerformanceWarning: new EventManager({
context,
module: "runtime",
event: "onPerformanceWarning",
extensionApi: this,
}).api(),

reload: async () => {
if (extension.upgrade) {

Expand Down
42 changes: 42 additions & 0 deletions toolkit/components/extensions/schemas/runtime.json
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,18 @@
"allowedContexts": ["content", "devtools"],
"description": "The reason that the event is being dispatched. 'app_update' is used when the restart is needed because the application is updated to a newer version. 'os_update' is used when the restart is needed because the browser/OS is updated to a newer version. 'periodic' is used when the system runs for more than the permitted uptime set in the enterprise policy.",
"enum": ["app_update", "os_update", "periodic"]
},
{
"id": "OnPerformanceWarningCategory",
"type": "string",
"enum": ["content_script"],
"description": "The performance warning event category, e.g. 'content_script'."
},
{
"id": "OnPerformanceWarningSeverity",
"type": "string",
"enum": ["low", "medium", "high"],
"description": "The performance warning event severity. Will be 'high' for serious and user-visible issues."
}
],
"properties": {
Expand Down Expand Up @@ -677,6 +689,36 @@
"description": "The reason that the event is being dispatched."
}
]
},
{
"name": "onPerformanceWarning",
"type": "function",
"description": "Fired when a runtime performance issue is detected with the extension. Observe this event to be proactively notified of runtime performance problems with the extension.",
"parameters": [
{
"type": "object",
"name": "details",
"properties": {
"category": {
"$ref": "OnPerformanceWarningCategory",
"description": "The performance warning event category, e.g. 'content_script'."
},
"severity": {
"$ref": "OnPerformanceWarningSeverity",
"description": "The performance warning event severity, e.g. 'high'."
},
"tabId": {
"type": "integer",
"optional": true,
"description": "The $(ref:tabs.Tab) that the performance warning relates to, if any."
},
"description": {
"type": "string",
"description": "An explanation of what the warning means, and hopefully how to address it."
}
}
}
]
}
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,16 @@ let expectedBackgroundApis = [
"runtime.onConnectExternal",
"runtime.onInstalled",
"runtime.onMessageExternal",
"runtime.onPerformanceWarning",
"runtime.onStartup",
"runtime.onSuspend",
"runtime.onSuspendCanceled",
"runtime.onUpdateAvailable",
"runtime.openOptionsPage",
"runtime.reload",
"runtime.setUninstallURL",
"runtime.OnPerformanceWarningCategory",
"runtime.OnPerformanceWarningSeverity",
"theme.getCurrent",
"theme.onUpdated",
"types.LevelOfControl",
Expand Down

0 comments on commit 51c2cb8

Please sign in to comment.