diff options
Diffstat (limited to 'devtools/client/framework/test/allocations')
22 files changed, 968 insertions, 0 deletions
diff --git a/devtools/client/framework/test/allocations/browser_allocations_browser_console.ini b/devtools/client/framework/test/allocations/browser_allocations_browser_console.ini new file mode 100644 index 0000000000..77aec9d502 --- /dev/null +++ b/devtools/client/framework/test/allocations/browser_allocations_browser_console.ini @@ -0,0 +1,11 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + !/devtools/shared/test-helpers/allocation-tracker.js + head.js + +# Each metrics tests is loaded in a separate .ini file. This way the test is executed +# individually, without any other test being executed before or after. +[browser_allocations_browser_console.js] +skip-if = os != 'linux' || debug || asan || ccov # Results should be platform agnostic - only run on linux64-opt diff --git a/devtools/client/framework/test/allocations/browser_allocations_browser_console.js b/devtools/client/framework/test/allocations/browser_allocations_browser_console.js new file mode 100644 index 0000000000..13d0171dfa --- /dev/null +++ b/devtools/client/framework/test/allocations/browser_allocations_browser_console.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Record allocations while opening and closing the Browser Console + +const TEST_URL = + "http://example.com/browser/devtools/client/framework/test/allocations/reloaded-page.html"; + +const { require } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" +); +const { + BrowserConsoleManager, +} = require("resource://devtools/client/webconsole/browser-console-manager.js"); + +async function testScript() { + // Open + await BrowserConsoleManager.toggleBrowserConsole(); + + // Reload the tab to make the test slightly more real + const hud = BrowserConsoleManager.getBrowserConsole(); + const onTargetProcessed = hud.commands.targetCommand.once( + "processed-available-target" + ); + + gBrowser.reloadTab(gBrowser.selectedTab); + + info("Wait for target of the new document to be fully processed"); + await onTargetProcessed; + + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Close + await BrowserConsoleManager.toggleBrowserConsole(); + + // Browser console still cleanup stuff after the resolution of toggleBrowserConsole. + // So wait for a little while to ensure it completes all cleanups. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 500)); +} + +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["devtools.browsertoolbox.scope", "everything"]], + }); + + const tab = await addTab(TEST_URL); + + // Run the test scenario first before recording in order to load all the + // modules. Otherwise they get reported as "still allocated" objects, + // whereas we do expect them to be kept in memory as they are loaded via + // the main DevTools loader, which keeps the module loaded until the + // shutdown of Firefox + await testScript(); + + // Now, run the test script. This time, we record this run. + await startRecordingAllocations(); + + for (let i = 0; i < 3; i++) { + await testScript(); + } + + await stopRecordingAllocations("browser-console"); + + gBrowser.removeTab(tab); +}); diff --git a/devtools/client/framework/test/allocations/browser_allocations_reload_debugger.ini b/devtools/client/framework/test/allocations/browser_allocations_reload_debugger.ini new file mode 100644 index 0000000000..339608ca83 --- /dev/null +++ b/devtools/client/framework/test/allocations/browser_allocations_reload_debugger.ini @@ -0,0 +1,14 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + !/devtools/shared/test-helpers/allocation-tracker.js + head.js + reload-test.js + reloaded-page.html + reloaded.png + +# Each metrics tests is loaded in a separate .ini file. This way the test is executed +# individually, without any other test being executed before or after. +[browser_allocations_reload_debugger.js] +skip-if = os != 'linux' || debug || asan || ccov # Results should be platform agnostic - only run on linux64-opt diff --git a/devtools/client/framework/test/allocations/browser_allocations_reload_debugger.js b/devtools/client/framework/test/allocations/browser_allocations_reload_debugger.js new file mode 100644 index 0000000000..748a5e906e --- /dev/null +++ b/devtools/client/framework/test/allocations/browser_allocations_reload_debugger.js @@ -0,0 +1,13 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Record allocations while reloading the page with the debugger opened + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/framework/test/allocations/reload-test.js", + this +); + +add_task(createPanelReloadTest("reload-debugger", "jsdebugger")); diff --git a/devtools/client/framework/test/allocations/browser_allocations_reload_inspector.ini b/devtools/client/framework/test/allocations/browser_allocations_reload_inspector.ini new file mode 100644 index 0000000000..9eca40e633 --- /dev/null +++ b/devtools/client/framework/test/allocations/browser_allocations_reload_inspector.ini @@ -0,0 +1,14 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + !/devtools/shared/test-helpers/allocation-tracker.js + head.js + reload-test.js + reloaded-page.html + reloaded.png + +# Each metrics tests is loaded in a separate .ini file. This way the test is executed +# individually, without any other test being executed before or after. +[browser_allocations_reload_inspector.js] +skip-if = os != 'linux' || debug || asan || ccov # Results should be platform agnostic - only run on linux64-opt diff --git a/devtools/client/framework/test/allocations/browser_allocations_reload_inspector.js b/devtools/client/framework/test/allocations/browser_allocations_reload_inspector.js new file mode 100644 index 0000000000..3369c54f24 --- /dev/null +++ b/devtools/client/framework/test/allocations/browser_allocations_reload_inspector.js @@ -0,0 +1,13 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Record allocations while reloading the page with the inspector opened + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/framework/test/allocations/reload-test.js", + this +); + +add_task(createPanelReloadTest("reload-inspector", "inspector")); diff --git a/devtools/client/framework/test/allocations/browser_allocations_reload_netmonitor.ini b/devtools/client/framework/test/allocations/browser_allocations_reload_netmonitor.ini new file mode 100644 index 0000000000..9323e9a8d9 --- /dev/null +++ b/devtools/client/framework/test/allocations/browser_allocations_reload_netmonitor.ini @@ -0,0 +1,14 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + !/devtools/shared/test-helpers/allocation-tracker.js + head.js + reload-test.js + reloaded-page.html + reloaded.png + +# Each metrics tests is loaded in a separate .ini file. This way the test is executed +# individually, without any other test being executed before or after. +[browser_allocations_reload_netmonitor.js] +skip-if = os != 'linux' || debug || asan || ccov # Results should be platform agnostic - only run on linux64-opt diff --git a/devtools/client/framework/test/allocations/browser_allocations_reload_netmonitor.js b/devtools/client/framework/test/allocations/browser_allocations_reload_netmonitor.js new file mode 100644 index 0000000000..2a57652ac5 --- /dev/null +++ b/devtools/client/framework/test/allocations/browser_allocations_reload_netmonitor.js @@ -0,0 +1,13 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Record allocations while reloading the page with the netmonitor opened + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/framework/test/allocations/reload-test.js", + this +); + +add_task(createPanelReloadTest("reload-netmonitor", "netmonitor")); diff --git a/devtools/client/framework/test/allocations/browser_allocations_reload_no_devtools.ini b/devtools/client/framework/test/allocations/browser_allocations_reload_no_devtools.ini new file mode 100644 index 0000000000..52f1ce809e --- /dev/null +++ b/devtools/client/framework/test/allocations/browser_allocations_reload_no_devtools.ini @@ -0,0 +1,13 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + !/devtools/shared/test-helpers/allocation-tracker.js + head.js + reloaded-page.html + reloaded.png + +# Each metrics tests is loaded in a separate .ini file. This way the test is executed +# individually, without any other test being executed before or after. +[browser_allocations_reload_no_devtools.js] +skip-if = os != 'linux' || debug || asan || ccov # Results should be platform agnostic - only run on linux64-opt diff --git a/devtools/client/framework/test/allocations/browser_allocations_reload_no_devtools.js b/devtools/client/framework/test/allocations/browser_allocations_reload_no_devtools.js new file mode 100644 index 0000000000..e2f344fcb5 --- /dev/null +++ b/devtools/client/framework/test/allocations/browser_allocations_reload_no_devtools.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Record allocations while reloading the page without anything related to DevTools running + +const TEST_URL = + "http://example.com/browser/devtools/client/framework/test/allocations/reloaded-page.html"; + +async function testScript() { + await BrowserTestUtils.reloadTab(gBrowser.selectedTab); + + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 1000)); +} + +add_task(async function () { + const tab = await addTab(TEST_URL); + + // Run the test scenario first before recording in order to load all the + // modules. Otherwise they get reported as "still allocated" objects, + // whereas we do expect them to be kept in memory as they are loaded via + // the main DevTools loader, which keeps the module loaded until the + // shutdown of Firefox + await testScript(); + + await startRecordingAllocations({ + alsoRecordContentProcess: true, + }); + + // Now, run the test script. This time, we record this run. + for (let i = 0; i < 10; i++) { + await testScript(); + } + + await stopRecordingAllocations("reload-no-devtools", { + alsoRecordContentProcess: true, + }); + + gBrowser.removeTab(tab); +}); diff --git a/devtools/client/framework/test/allocations/browser_allocations_reload_webconsole.ini b/devtools/client/framework/test/allocations/browser_allocations_reload_webconsole.ini new file mode 100644 index 0000000000..baac713e94 --- /dev/null +++ b/devtools/client/framework/test/allocations/browser_allocations_reload_webconsole.ini @@ -0,0 +1,14 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + !/devtools/shared/test-helpers/allocation-tracker.js + head.js + reload-test.js + reloaded-page.html + reloaded.png + +# Each metrics tests is loaded in a separate .ini file. This way the test is executed +# individually, without any other test being executed before or after. +[browser_allocations_reload_webconsole.js] +skip-if = os != 'linux' || debug || asan || ccov # Results should be platform agnostic - only run on linux64-opt diff --git a/devtools/client/framework/test/allocations/browser_allocations_reload_webconsole.js b/devtools/client/framework/test/allocations/browser_allocations_reload_webconsole.js new file mode 100644 index 0000000000..a60fd03b3c --- /dev/null +++ b/devtools/client/framework/test/allocations/browser_allocations_reload_webconsole.js @@ -0,0 +1,13 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Record allocations while reloading the page with the webconsole opened + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/framework/test/allocations/reload-test.js", + this +); + +add_task(createPanelReloadTest("reload-webconsole", "webconsole")); diff --git a/devtools/client/framework/test/allocations/browser_allocations_target.ini b/devtools/client/framework/test/allocations/browser_allocations_target.ini new file mode 100644 index 0000000000..4658423b66 --- /dev/null +++ b/devtools/client/framework/test/allocations/browser_allocations_target.ini @@ -0,0 +1,11 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + !/devtools/shared/test-helpers/allocation-tracker.js + head.js + +# Each metrics tests is loaded in a separate .ini file. This way the test is executed +# individually, without any other test being executed before or after. +[browser_allocations_target.js] +skip-if = os != 'linux' || debug || asan || ccov # Results should be platform agnostic - only run on linux64-opt diff --git a/devtools/client/framework/test/allocations/browser_allocations_target.js b/devtools/client/framework/test/allocations/browser_allocations_target.js new file mode 100644 index 0000000000..a93d6b51c9 --- /dev/null +++ b/devtools/client/framework/test/allocations/browser_allocations_target.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Record allocations while spawning Commands and the first top level target + +const TEST_URL = + "data:text/html;charset=UTF-8,<div>Target allocations test</div>"; + +const { require } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" +); +const { + CommandsFactory, +} = require("resource://devtools/shared/commands/commands-factory.js"); + +async function testScript(tab) { + const commands = await CommandsFactory.forTab(tab); + await commands.targetCommand.startListening(); + + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 1000)); + + // destroy the commands to also destroy the dedicated client. + await commands.destroy(); + + // Spin the event loop to ensure commands destruction is fully completed + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 0)); +} + +add_task(async function () { + const tab = await addTab(TEST_URL); + + // Run the test scenario first before recording in order to load all the + // modules. Otherwise they get reported as "still allocated" objects, + // whereas we do expect them to be kept in memory as they are loaded via + // the main DevTools loader, which keeps the module loaded until the + // shutdown of Firefox + await testScript(tab); + + await startRecordingAllocations(); + + // Now, run the test script. This time, we record this run. + await testScript(tab); + + await stopRecordingAllocations("target"); + + gBrowser.removeTab(tab); +}); diff --git a/devtools/client/framework/test/allocations/browser_allocations_toolbox.ini b/devtools/client/framework/test/allocations/browser_allocations_toolbox.ini new file mode 100644 index 0000000000..2996ef7767 --- /dev/null +++ b/devtools/client/framework/test/allocations/browser_allocations_toolbox.ini @@ -0,0 +1,11 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +support-files = + !/devtools/shared/test-helpers/allocation-tracker.js + head.js + +# Each metrics tests is loaded in a separate .ini file. This way the test is executed +# individually, without any other test being executed before or after. +[browser_allocations_toolbox.js] +skip-if = os != 'linux' || debug || asan || ccov # Results should be platform agnostic - only run on linux64-opt diff --git a/devtools/client/framework/test/allocations/browser_allocations_toolbox.js b/devtools/client/framework/test/allocations/browser_allocations_toolbox.js new file mode 100644 index 0000000000..e0f86511bc --- /dev/null +++ b/devtools/client/framework/test/allocations/browser_allocations_toolbox.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Record allocations while opening and closing DevTools + +const TEST_URL = + "data:text/html;charset=UTF-8,<div>Target allocations test</div>"; + +const { require } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" +); +const { + gDevTools, +} = require("resource://devtools/client/framework/devtools.js"); + +async function testScript(tab) { + const toolbox = await gDevTools.showToolboxForTab(tab, { + toolId: "inspector", + }); + + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 1000)); + + await toolbox.destroy(); + + // Spin the event loop to ensure toolbox destroy is fully completed + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 0)); +} + +add_task(async function () { + const tab = await addTab(TEST_URL); + + // Run the test scenario first before recording in order to load all the + // modules. Otherwise they get reported as "still allocated" objects, + // whereas we do expect them to be kept in memory as they are loaded via + // the main DevTools loader, which keeps the module loaded until the + // shutdown of Firefox + await testScript(tab); + + await startRecordingAllocations(); + + // Now, run the test script. This time, we record this run. + for (let i = 0; i < 3; i++) { + await testScript(tab); + } + + await stopRecordingAllocations("toolbox"); + + gBrowser.removeTab(tab); +}); diff --git a/devtools/client/framework/test/allocations/docs/index.md b/devtools/client/framework/test/allocations/docs/index.md new file mode 100644 index 0000000000..f0d6921325 --- /dev/null +++ b/devtools/client/framework/test/allocations/docs/index.md @@ -0,0 +1,241 @@ +# Allocation tests + +The [allocations](https://searchfox.org/mozilla-central/source/devtools/client/framework/test/allocations) folder contains special mochitests which are meant to record data about the memory usage of DevTools. +This uses Spidermonkey's Memory API implemented next to the debugger API. +For more info, see the following doc: +<https://searchfox.org/mozilla-central/source/js/src/doc/Debugger/Debugger.Memory.md> + +# Test example + +```javascript +add_task(async function() { + // Execute preliminary setup in order to be able to run your scenario + // You would typicaly load modules, open a tab, a toolbox, ... + ... + + // Run the test scenario first before recording in order to load all the + // modules. Otherwise they get reported as "still allocated" objects, + // whereas we do expect them to be kept in memory as they are loaded via + // the main DevTools loader, which keeps the module loaded until the + // shutdown of Firefox + await testScript(); + + // Pass alsoRecordContentProcess if you want to record the content process + // of the current tab. Otherwise it will only record parent process objects. + await startRecordingAllocations({ alsoRecordContentProcess: true }); + + // Now, run the test script. This time, we record this run. + await testScript(toolbox); + + // This will stop the record and also publish the results to Talos database + // Second argument will be the name of the test displayed in Talos. + // Many tests will be recorded, but all of them will be prefixed with this string. + await stopRecordingAllocations("reload", { alsoRecordContentProcess: true }); + + // Then, here you can execute cleanup. + // You would typically close the tab, toolbox, ... +}); +``` + +# How to run them locally + +```bash +$ ./mach mochitest --headless devtools/client/framework/test/allocations/ +``` + +And to only see the results: +```bash +$ ./mach mochitest --headless devtools/client/framework/test/allocations/ | grep " test leaked " +``` + +# Debug leaks + +If you are seeing a regression or an improvement, only seeing the number of objects being leaked isn't super helpful. +The tests includes some special debug modes which are printing lots of data to figure out what is leaking and why. + +You may run the test with the following env variable to turn debug mode on: +```bash +DEBUG_DEVTOOLS_ALLOCATIONS=leak|allocations $ ./mach mochitest --headless devtools/client/framework/test/allocations/the-fault-test.js +``` + +**DEBUG_DEVTOOLS_ALLOCATIONS** can enable two distinct debug output. (Only one can be enabled at a given time) + +**DEBUG_DEVTOOLS_ALLOCATIONS=allocations** will report all allocation sites that have been made +while running your test. This will include allocations which has been freed. +This view is especially useful if you want to reduce allocations in order to reduce GC overload. + +**DEBUG_DEVTOOLS_ALLOCATIONS=leak** will report only the allocations which are still allocated +at the end of your test. Sometimes it will only report allocations with missing stack trace. +Thus making the preview view helpful. + +## Example + +Let's assume we have the following code: + +```javascript + 1: exports.MyModule = { + 2: globalArray: [], + 3: test() { + 3: // The following object will be allocated but not leaked, + 5: // as we keep no reference to it anywhere + 6: const transientObject = {}; + 7: + 8: // The following object will be allocated on this line, + 9: // but leaked on the following one. By storing a reference +10: // to it in the global array which is never cleared. +11: const leakedObject = {}; +12: this.globalArray.push(leakedObject); +13: }, +14: }; +``` + +And that, we have a memory test doing this: + +```javascript + const { MyModule } = require("devtools/my-module"); + + await startRecordingAllocations(); + + MyModule.test(); + + await stopRecordingAllocations("target"); +``` + +We can first review all the allocations by running: + +```bash +DEBUG_DEVTOOLS_ALLOCATIONS=allocations $ ./mach mochitest --headless devtools/client/framework/test/allocations/browser_allocation_myTest.js + +``` + +which will print at the end: + +```javascript +DEVTOOLS ALLOCATION: all allocations (which may be freed or are still allocated): +[ + { + "src": "UNKNOWN", + "count": 80, + "lines": [ + "?: 80" + ] + }, + { + "src": "resource://devtools/my-module.js", + "count": 2, + "lines": [ + "11: 1" + "6: 1" + ] + } +] +``` + +The first part, with `UNKNOWN` can be ignored. This is about objects with missing allocation sites. +The second part of this logs tells us that 2 objects were allocated from my-module.js when running the test. +One has been allocated at line 6, it is `transcientObject`. +Another one has been allocated at line 11, it is `leakedObject`. + +Now, we can use the second view to focus only on objects that have been kept allocated: + +```bash +DEBUG_DEVTOOLS_ALLOCATIONS=leaks $ ./mach mochitest --headless devtools/client/framework/test/allocations/browser_allocation_myTest.js + +``` + +which will print at the end: + +```javascript +DEVTOOLS ALLOCATION: allocations which leaked: +[ + { + "src": "UNKNOWN", + "count": 80, + "lines": [ + "?: 80" + ] + }, + { + "src": "resource://devtools/shared/commands/commands-factory.js", + "count": 1, + "lines": [ + "11: 1" + ] + } +] +``` + +Similarly, we can focus only on the second part, which tells us that only one object is being leaked +and this object has been originally created from line 11, this is `leakedObject`. +This doesn't tell us why the object is being kept allocated, but at least we know which one is being kept in memory. + + +## Debug leaks via dominators + +This last feature might be the most powerful and isn't bound to DEBUG_DEVTOOLS_ALLOCATIONS. +This is always enabled. +Also, it requires to know which particular object is being leaked and also require to hack +the codebase in order to pass a reference of the suspicious object to the test helper. + +You can instruct the test helper to track a given object by doing this: + +```javascript + 1: // Let's say it is some code running from "my-module.js" + 2: + 3: // From a DevTools CommonJS module: + 4: const { track } = require("devtools/shared/test-helpers/tracked-objects.sys.mjs"); + 5: // From anything else, JSM, XPCOM module,...: + 6: const { track } = ChromeUtils.importESModule("resource://devtools/shared/test-helpers/tracked-objects.sys.mjs"); + 7: + 8: const g = []; + 9: function someFunctionInDevToolsCalledBySomething() { +10: const myLeakedObject = {}; +11: track(myLeakedObject); +12: +13: // Simulate a leak by holding a reference to the object in a global `g` array +14: g.push({ seeMyCustomAttributeHere: myLeakedObject }); +15: } +``` + +Then, when running the test you will get such output: + +```bash + 0:41.26 GECKO(644653) # Tracing: Object@my-module:10 + 0:40.65 GECKO(644653) ### Path(s) from root: + 0:41.26 GECKO(644653) - other@no-stack:undefined.WeakMap entry value + 0:41.26 GECKO(644653) \--> LexicalEnvironment@base-loader.sys.mjs:160.**UNKNOWN SLOT 1** + 0:41.26 GECKO(644653) \--> Object@base-loader.sys.mjs:155.g + 0:41.26 GECKO(644653) \--> Array@my-module.js:8.objectElements[0] + 0:41.26 GECKO(644653) \--> Object@my-module.js:14.seeMyCustomAttributeHere + 0:41.26 GECKO(644653) \--> Object@my-module.js:10 +``` + +This output means that `myLeakedObject` was originally allocated from my-module.js at line 10. +And is being held allocated because it is kept in an Object allocated from my-module.js at line 14. +This is our custom object we stored in `g` global Array. +This custom object it hold by the Array allocated at line 8 of my-module.js. +And this array is held allocated from an Object, itself allocated by base-loader.sys.mjs at line 155. +This is the global of the my-module.js's module, created by DevTools loader. +Then we see some more low level object up to another global object, which misses its allocation site. + +# How to easily get data from try run + +```bash +$ ./mach try fuzzy devtools/client/framework/test/allocations/ --query "'linux 'chrome-e10s 'opt '64-qr/opt" +``` + +You might also pass `--rebuild 3` if the test result is having some noise and you want more test runs. + +# Following trends for these tests + +You may try looking at: +<https://firefox-dev.tools/performance-dashboard/tools/memory.html> + +Or at: +<https://treeherder.mozilla.org/perfherder/graphs?highlightAlerts=1&highlightChangelogData=1&series=autoland,3887143,1,12&series=mozilla-central,3887737,1,12&series=mozilla-central,3887740,1,12&series=mozilla-central,3887743,1,12&series=mozilla-central,3896204,1,12&timerange=2592000&zoom=1630504360002,1632239562424,0,123469.11111111111> + +Link that you get from: <https://treeherder.mozilla.org/perfherder/graphs> +by looking at last year data for "DevTools" in the first dropdown, +and double clicking on the relevant line in "Tests" menulist. + +Significant improvements and regressions will be notified through [the following dashboard](https://treeherder.mozilla.org/perfherder/alerts?hideDwnToInv=1&page=1&framework=12). diff --git a/devtools/client/framework/test/allocations/head.js b/devtools/client/framework/test/allocations/head.js new file mode 100644 index 0000000000..d9bded321a --- /dev/null +++ b/devtools/client/framework/test/allocations/head.js @@ -0,0 +1,250 @@ +/* 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"; + +// Load the tracker very first in order to ensure tracking all objects created by DevTools. +// This is especially important for allocation sites. We need to catch the global the +// earliest possible in order to ensure that all allocation objects come with a stack. +// +// If we want to track DevTools module loader we should ensure loading Loader.sys.mjs within +// the `testScript` Function. i.e. after having calling startRecordingAllocations. +let tracker, releaseTrackerLoader; +{ + const { + useDistinctSystemPrincipalLoader, + releaseDistinctSystemPrincipalLoader, + } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs" + ); + + const requester = {}; + const loader = useDistinctSystemPrincipalLoader(requester); + releaseTrackerLoader = () => releaseDistinctSystemPrincipalLoader(requester); + const { allocationTracker } = loader.require( + "chrome://mochitests/content/browser/devtools/shared/test-helpers/allocation-tracker.js" + ); + tracker = allocationTracker({ watchDevToolsGlobals: true }); +} + +// /!\ Be careful about imports/require +// +// Some tests may record the very first time we load a module. +// If we start loading them from here, we might only retrieve the already loaded +// module from the loader's cache. This would no longer highlight the cost +// of loading a new module from scratch. +// +// => Avoid loading devtools module as much as possible +// => If you really have to, lazy load them + +XPCOMUtils.defineLazyGetter(this, "TrackedObjects", () => { + return ChromeUtils.importESModule( + "resource://devtools/shared/test-helpers/tracked-objects.sys.mjs" + ); +}); + +// So that PERFHERDER data can be extracted from the logs. +SimpleTest.requestCompleteLog(); + +// We have to disable testing mode, or various debug instructions are enabled. +// We especially want to disable redux store history, which would leak all the actions! +SpecialPowers.pushPrefEnv({ + set: [["devtools.testing", false]], +}); + +// Set DEBUG_DEVTOOLS_ALLOCATIONS=allocations|leaks in order print debug informations. +const DEBUG_ALLOCATIONS = Services.env.get("DEBUG_DEVTOOLS_ALLOCATIONS"); + +async function addTab(url) { + const tab = BrowserTestUtils.addTab(gBrowser, url); + gBrowser.selectedTab = tab; + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + return tab; +} + +/** + * This function will force some garbage collection before recording + * data about allocated objects. + * + * This accept an optional boolean to also record the content process objects + * of the current tab. That, in addition of objects from the parent process, + * which are always recorded. + * + * This return same data object which is meant to be passed to `stopRecordingAllocations` as-is. + * + * See README.md file in this folder. + */ +async function startRecordingAllocations({ + alsoRecordContentProcess = false, +} = {}) { + // Also start recording allocations in the content process, if requested + if (alsoRecordContentProcess) { + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [DEBUG_ALLOCATIONS], + async debug_allocations => { + const { DevToolsLoader } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" + ); + + const { + useDistinctSystemPrincipalLoader, + releaseDistinctSystemPrincipalLoader, + } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs" + ); + + const requester = {}; + const loader = useDistinctSystemPrincipalLoader(requester); + const { allocationTracker } = loader.require( + "chrome://mochitests/content/browser/devtools/shared/test-helpers/allocation-tracker.js" + ); + // We watch all globals in the content process, (instead of only DevTools global in parent process) + // because we may easily leak web page objects, which aren't in DevTools global. + const tracker = allocationTracker({ watchAllGlobals: true }); + + // /!\ HACK: store tracker and releaseTrackerLoader on DevToolsLoader in order + // to be able to reuse them in a following call to SpecialPowers.spawn + DevToolsLoader.tracker = tracker; + DevToolsLoader.releaseTrackerLoader = () => + releaseDistinctSystemPrincipalLoader(requester); + + await tracker.startRecordingAllocations(debug_allocations); + } + ); + // Trigger a GC in the parent process as this additional ContentTask + // seems to make harder to release objects created before we start recording. + await tracker.doGC(); + } + + await tracker.startRecordingAllocations(DEBUG_ALLOCATIONS); +} + +/** + * See doc of startRecordingAllocations + */ +async function stopRecordingAllocations( + recordName, + { alsoRecordContentProcess = false } = {} +) { + // Ensure that Memory API didn't ran out of buffers + ok(!tracker.overflowed, "Allocation were all recorded in the parent process"); + + // And finally, retrieve the record *after* having ran the test + const parentProcessData = await tracker.stopRecordingAllocations( + DEBUG_ALLOCATIONS + ); + + const objectNodeIds = TrackedObjects.getAllNodeIds(); + if (objectNodeIds.length) { + tracker.traceObjects(objectNodeIds); + } + + let contentProcessData = null; + if (alsoRecordContentProcess) { + contentProcessData = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [DEBUG_ALLOCATIONS], + debug_allocations => { + const { DevToolsLoader } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" + ); + const { tracker } = DevToolsLoader; + ok( + !tracker.overflowed, + "Allocation were all recorded in the content process" + ); + return tracker.stopRecordingAllocations(debug_allocations); + } + ); + } + + const trackedObjectsInContent = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => { + const TrackedObjects = ChromeUtils.importESModule( + "resource://devtools/shared/test-helpers/tracked-objects.sys.mjs" + ); + const objectNodeIds = TrackedObjects.getAllNodeIds(); + if (objectNodeIds.length) { + const { DevToolsLoader } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" + ); + const { tracker } = DevToolsLoader; + // Record the heap snapshot from the content process, + // and pass the record's filepath to the parent process + // As only the parent process can read the file because + // of sandbox restrictions made to content processes regarding file I/O. + const snapshotFile = tracker.getSnapshotFile(); + return { snapshotFile, objectNodeIds }; + } + return null; + } + ); + if (trackedObjectsInContent) { + tracker.traceObjects( + trackedObjectsInContent.objectNodeIds, + trackedObjectsInContent.snapshotFile + ); + } + + // Craft the JSON object required to save data in talos database + info( + `The ${recordName} test leaked ${parentProcessData.objectsWithStack} objects (${parentProcessData.objectsWithoutStack} with missing allocation site) in the parent process` + ); + const PERFHERDER_DATA = { + framework: { + name: "devtools", + }, + suites: [ + { + name: recordName + ":parent-process", + subtests: [ + { + name: "objects-with-stacks", + value: parentProcessData.objectsWithStack, + }, + { + name: "memory", + value: parentProcessData.memory, + }, + ], + }, + ], + }; + if (alsoRecordContentProcess) { + info( + `The ${recordName} test leaked ${contentProcessData.objectsWithStack} objects (${contentProcessData.objectsWithoutStack} with missing allocation site) in the content process` + ); + PERFHERDER_DATA.suites.push({ + name: recordName + ":content-process", + subtests: [ + { + name: "objects-with-stacks", + value: contentProcessData.objectsWithStack, + }, + { + name: "memory", + value: contentProcessData.memory, + }, + ], + }); + + // Finally release the tracker loader in content process. + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + const { DevToolsLoader } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" + ); + DevToolsLoader.releaseTrackerLoader(); + }); + } + + // And release the tracker loader in the parent process + releaseTrackerLoader(); + + // Log it to stdout so that perfherder can collect this data. + // This only works if we called `SimpleTest.requestCompleteLog()`! + info("PERFHERDER_DATA: " + JSON.stringify(PERFHERDER_DATA)); +} diff --git a/devtools/client/framework/test/allocations/moz.build b/devtools/client/framework/test/allocations/moz.build new file mode 100644 index 0000000000..37358a9075 --- /dev/null +++ b/devtools/client/framework/test/allocations/moz.build @@ -0,0 +1,16 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +BROWSER_CHROME_MANIFESTS += [ + "browser_allocations_browser_console.ini", + "browser_allocations_reload_debugger.ini", + "browser_allocations_reload_inspector.ini", + "browser_allocations_reload_netmonitor.ini", + "browser_allocations_reload_no_devtools.ini", + "browser_allocations_reload_webconsole.ini", + "browser_allocations_target.ini", + "browser_allocations_toolbox.ini", +] diff --git a/devtools/client/framework/test/allocations/reload-test.js b/devtools/client/framework/test/allocations/reload-test.js new file mode 100644 index 0000000000..a382998dd3 --- /dev/null +++ b/devtools/client/framework/test/allocations/reload-test.js @@ -0,0 +1,81 @@ +/* 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"; + +/* import-globals-from head.js */ +/* eslint no-unused-vars: [2, {"vars": "local"}] */ + +/** + * Generate a test Task to record allocation when reloading a test page + * while having one particular DevTools panel opened + * + * @param String recordName + * Name of the test recorded into PerfHerder/Talos database + * @param String toolId + * ID of the panel to open + */ +function createPanelReloadTest(recordName, toolId) { + return async function panelReloadTest() { + const TEST_URL = + "http://example.com/browser/devtools/client/framework/test/allocations/reloaded-page.html"; + + async function testScript(toolbox) { + const onTargetSwitched = + toolbox.commands.targetCommand.once("switched-target"); + const onReloaded = toolbox.getCurrentPanel().once("reloaded"); + + gBrowser.reloadTab(gBrowser.selectedTab); + + if ( + toolbox.commands.targetCommand.targetFront.targetForm + .followWindowGlobalLifeCycle + ) { + info("Wait for target switched"); + await onTargetSwitched; + } + + info("Wait for panel reload"); + await onReloaded; + + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + const tab = await addTab(TEST_URL); + + const { require } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" + ); + const { + gDevTools, + } = require("resource://devtools/client/framework/devtools.js"); + const toolbox = await gDevTools.showToolboxForTab(tab, { + toolId, + }); + + // Run the test scenario first before recording in order to load all the + // modules. Otherwise they get reported as "still allocated" objects, + // whereas we do expect them to be kept in memory as they are loaded via + // the main DevTools loader, which keeps the module loaded until the + // shutdown of Firefox + await testScript(toolbox); + + await startRecordingAllocations({ + alsoRecordContentProcess: true, + }); + + // Now, run the test script. This time, we record this run. + for (let i = 0; i < 10; i++) { + await testScript(toolbox); + } + + await stopRecordingAllocations(recordName, { + alsoRecordContentProcess: true, + }); + + await toolbox.destroy(); + gBrowser.removeTab(tab); + }; +} diff --git a/devtools/client/framework/test/allocations/reloaded-page.html b/devtools/client/framework/test/allocations/reloaded-page.html new file mode 100644 index 0000000000..4f14c8a0c3 --- /dev/null +++ b/devtools/client/framework/test/allocations/reloaded-page.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> + <head> + <title>Reloaded page</title> + <meta charset="UTF-8"> + </head> + <body> + The reloaded page + <img src="reloaded.png" /> + </body> +</html> diff --git a/devtools/client/framework/test/allocations/reloaded.png b/devtools/client/framework/test/allocations/reloaded.png Binary files differnew file mode 100644 index 0000000000..769c636340 --- /dev/null +++ b/devtools/client/framework/test/allocations/reloaded.png |