diff options
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.js | 277 |
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(); +}); |