summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/test/xpcshell/test_ext_contexts_gc.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /toolkit/components/extensions/test/xpcshell/test_ext_contexts_gc.js
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/extensions/test/xpcshell/test_ext_contexts_gc.js')
-rw-r--r--toolkit/components/extensions/test/xpcshell/test_ext_contexts_gc.js277
1 files changed, 277 insertions, 0 deletions
diff --git a/toolkit/components/extensions/test/xpcshell/test_ext_contexts_gc.js b/toolkit/components/extensions/test/xpcshell/test_ext_contexts_gc.js
new file mode 100644
index 0000000000..a828584ced
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_contexts_gc.js
@@ -0,0 +1,277 @@
+"use strict";
+
+const server = createHttpServer();
+server.registerDirectory("/data/", do_get_file("data"));
+
+const BASE_URL = `http://localhost:${server.identity.primaryPort}/data`;
+
+// ExtensionContent.jsm needs to know when it's running from xpcshell,
+// to use the right timeout for content scripts executed at document_idle.
+ExtensionTestUtils.mockAppInfo();
+
+// Each of these tests do the following:
+// 1. Load document to create an extension context (instance of BaseContext).
+// 2. Get weak reference to that context.
+// 3. Unload the document.
+// 4. Force GC and check that the weak reference has been invalidated.
+
+async function reloadTopContext(contentPage) {
+ await contentPage.legacySpawn(null, async () => {
+ let { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+ );
+ let windowNukeObserved = TestUtils.topicObserved("inner-window-nuked");
+ info(`Reloading top-level document`);
+ this.content.location.reload();
+ await windowNukeObserved;
+ info(`Reloaded top-level document`);
+ });
+}
+
+async function assertContextReleased(contentPage, description) {
+ await contentPage.legacySpawn(description, async assertionDescription => {
+ // Force GC, see https://searchfox.org/mozilla-central/rev/b0275bc977ad7fda615ef34b822bba938f2b16fd/testing/talos/talos/tests/devtools/addon/content/damp.js#84-98
+ // and https://searchfox.org/mozilla-central/rev/33c21c060b7f3a52477a73d06ebcb2bf313c4431/xpcom/base/nsMemoryReporterManager.cpp#2574-2585,2591-2594
+ let gcCount = 0;
+ while (gcCount < 30 && this.contextWeakRef.get() !== null) {
+ ++gcCount;
+ // The JS engine will sometimes hold IC stubs for function
+ // environments alive across multiple CCs, which can keep
+ // closed-over JS objects alive. A shrinking GC will throw those
+ // stubs away, and therefore side-step the problem.
+ Cu.forceShrinkingGC();
+ Cu.forceCC();
+ Cu.forceGC();
+ await new Promise(resolve => this.content.setTimeout(resolve, 0));
+ }
+
+ // The above loop needs to be repeated at most 3 times according to MinimizeMemoryUsage:
+ // https://searchfox.org/mozilla-central/rev/6f86cc3479f80ace97f62634e2c82a483d1ede40/xpcom/base/nsMemoryReporterManager.cpp#2644-2647
+ Assert.lessOrEqual(
+ gcCount,
+ 3,
+ `Context should have been GCd within a few GC attempts.`
+ );
+
+ // Each test will set this.contextWeakRef before unloading the document.
+ Assert.ok(!this.contextWeakRef.get(), assertionDescription);
+ });
+}
+
+add_task(async function test_ContentScriptContextChild_in_child_frame() {
+ let extensionData = {
+ manifest: {
+ content_scripts: [
+ {
+ matches: ["http://*/*/file_iframe.html"],
+ js: ["content_script.js"],
+ all_frames: true,
+ },
+ ],
+ },
+
+ files: {
+ "content_script.js": "browser.test.sendMessage('contentScriptLoaded');",
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+
+ let contentPage = await ExtensionTestUtils.loadContentPage(
+ `${BASE_URL}/file_toplevel.html`
+ );
+ await extension.awaitMessage("contentScriptLoaded");
+
+ await contentPage.legacySpawn(extension.id, async extensionId => {
+ const { ExtensionContent } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionContent.sys.mjs"
+ );
+ let frame = this.content.document.querySelector(
+ "iframe[src*='file_iframe.html']"
+ );
+ let context = ExtensionContent.getContextByExtensionId(
+ extensionId,
+ frame.contentWindow
+ );
+
+ Assert.ok(!!context, "Got content script context");
+
+ this.contextWeakRef = Cu.getWeakReference(context);
+ frame.remove();
+ });
+
+ await assertContextReleased(
+ contentPage,
+ "ContentScriptContextChild should have been released"
+ );
+
+ await contentPage.close();
+ await extension.unload();
+});
+
+add_task(async function test_ContentScriptContextChild_in_toplevel() {
+ let extensionData = {
+ manifest: {
+ content_scripts: [
+ {
+ matches: ["http://*/*/file_sample.html"],
+ js: ["content_script.js"],
+ all_frames: true,
+ },
+ ],
+ },
+
+ files: {
+ "content_script.js": "browser.test.sendMessage('contentScriptLoaded');",
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+
+ let contentPage = await ExtensionTestUtils.loadContentPage(
+ `${BASE_URL}/file_sample.html`
+ );
+ await extension.awaitMessage("contentScriptLoaded");
+
+ await contentPage.legacySpawn(extension.id, async extensionId => {
+ const { ExtensionContent } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionContent.sys.mjs"
+ );
+ let context = ExtensionContent.getContextByExtensionId(
+ extensionId,
+ this.content
+ );
+
+ Assert.ok(!!context, "Got content script context");
+
+ this.contextWeakRef = Cu.getWeakReference(context);
+ });
+
+ await reloadTopContext(contentPage);
+ await extension.awaitMessage("contentScriptLoaded");
+ await assertContextReleased(
+ contentPage,
+ "ContentScriptContextChild should have been released"
+ );
+
+ await contentPage.close();
+ await extension.unload();
+});
+
+add_task(async function test_ExtensionPageContextChild_in_child_frame() {
+ let extensionData = {
+ files: {
+ "iframe.html": `
+ <!DOCTYPE html><meta charset="utf8">
+ <script src="script.js"></script>
+ `,
+ "toplevel.html": `
+ <!DOCTYPE html><meta charset="utf8">
+ <iframe src="iframe.html"></iframe>
+ `,
+ "script.js": "browser.test.sendMessage('extensionPageLoaded');",
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+
+ let contentPage = await ExtensionTestUtils.loadContentPage(
+ `moz-extension://${extension.uuid}/toplevel.html`,
+ {
+ extension,
+ remote: extension.extension.remote,
+ }
+ );
+ await extension.awaitMessage("extensionPageLoaded");
+
+ await contentPage.legacySpawn(extension.id, async extensionId => {
+ let { ExtensionPageChild } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionPageChild.sys.mjs"
+ );
+
+ let frame = this.content.document.querySelector(
+ "iframe[src*='iframe.html']"
+ );
+ let innerWindowID =
+ frame.browsingContext.currentWindowContext.innerWindowId;
+ let context = ExtensionPageChild.extensionContexts.get(innerWindowID);
+
+ Assert.ok(!!context, "Got extension page context for child frame");
+
+ this.contextWeakRef = Cu.getWeakReference(context);
+ frame.remove();
+ });
+
+ await assertContextReleased(
+ contentPage,
+ "ExtensionPageContextChild should have been released"
+ );
+
+ await contentPage.close();
+ await extension.unload();
+});
+
+add_task(async function test_ExtensionPageContextChild_in_toplevel() {
+ let extensionData = {
+ files: {
+ "toplevel.html": `
+ <!DOCTYPE html><meta charset="utf8">
+ <script src="script.js"></script>
+ `,
+ "script.js": "browser.test.sendMessage('extensionPageLoaded');",
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+
+ let contentPage = await ExtensionTestUtils.loadContentPage(
+ `moz-extension://${extension.uuid}/toplevel.html`,
+ {
+ extension,
+ remote: extension.extension.remote,
+ }
+ );
+ await extension.awaitMessage("extensionPageLoaded");
+
+ await contentPage.legacySpawn(extension.id, async extensionId => {
+ let { ExtensionPageChild } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionPageChild.sys.mjs"
+ );
+
+ let innerWindowID = this.content.windowGlobalChild.innerWindowId;
+ let context = ExtensionPageChild.extensionContexts.get(innerWindowID);
+
+ Assert.ok(!!context, "Got extension page context for top-level document");
+
+ this.contextWeakRef = Cu.getWeakReference(context);
+ });
+
+ await reloadTopContext(contentPage);
+ await extension.awaitMessage("extensionPageLoaded");
+ // For some unknown reason, the context cannot forcidbly be released by the
+ // garbage collector unless we wait for a short while.
+ await contentPage.spawn([], async () => {
+ let start = Date.now();
+ // The treshold was found after running this subtest only, 300 times
+ // in a release build (100 of xpcshell, xpcshell-e10s and xpcshell-remote).
+ // With treshold 8, almost half of the tests complete after a 17-18 ms delay.
+ // With treshold 7, over half of the tests complete after a 13-14 ms delay,
+ // with 12 failures in 300 tests runs.
+ // Let's double that number to have a safety margin.
+ for (let i = 0; i < 15; ++i) {
+ await new Promise(resolve => this.content.setTimeout(resolve, 0));
+ }
+ info(`Going to GC after waiting for ${Date.now() - start} ms.`);
+ });
+ await assertContextReleased(
+ contentPage,
+ "ExtensionPageContextChild should have been released"
+ );
+
+ await contentPage.close();
+ await extension.unload();
+});