"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": ` `, "toplevel.html": ` `, "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 () => { 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": ` `, "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 () => { 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(); });