Detect Oversized Object
This is a tutorial demonstrating how to detect oversized objects that are not released by a web app.
We recommend reading Detect Leaks in a Demo App, which will walk you through how to interpret memlab results and debug leak traces.
Set up the Example Web App Under Test
The demo app leaks a big array in each React rendering call (through the unregistered event handler and the closure scope chain).
/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * @nolint * @oncall web_perf_infra */import Link from 'next/link';import React, {useEffect} from 'react';export default function OversizedObject() { const bigArray = Array(1024 * 1024 * 2).fill(0); const eventHandler = () => { // the eventHandler closure keeps a reference // to the bigArray in the outter scope console.log('Using hugeObject', bigArray); }; useEffect(() => { // eventHandler is never unregistered window.addEventListener('custom-click', eventHandler); }, []); return ( <div className="container"> <div className="row"> <Link href="/">Go back</Link> </div> <br /> <div className="row"> Object<code>bigArray</code>is leaked. Please check <code>Memory</code>{' '} tab in devtools </div> </div> );}
Source file: packages/e2e/static/example/pages/examples/oversized-object.jsx
1. Clone Repo
To run the demo web app on you local machine, clone the
memlab
github repo:
git clone git@github.com:facebook/memlab.git
2. Run the Example App
Once you have cloned the repo on your local machine, run the following commands
from the root directory of the memlab
project:
npm install && npm run build
cd packages/e2e/static/example
npm install && npm run dev
This will spin up an example Nextjs app. Let's make sure it is running by visiting from your browser http://localhost:3000:
note
The port number :3000
may be different in your case.
First Attempt to Find Leaks
1. Create a Scenario File
/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * @nolint * @oncall web_perf_infra */function url() { return 'http://localhost:3000/';}// action where you suspect the memory leak might be happeningasync function action(page) { await page.click('a[href="/examples/oversized-object"]');}// how to go back to the state before actionwasync function back(page) { await page.click('a[href="/"]');}module.exports = {action, back, url};
Let's save this file as ~/memlab/scenarios/oversized-object.js
.
2. Run memlab
This will take a few minutes.
memlab run --scenario ~/memlab/scenarios/oversized-object.js
This time memlab didn't find any memory leaks.
The current built-in Leak detector only considers objects that meet all of the following criteria to be memory leaks:
- The object is allocated by interaction triggered by the
action
callback. - The object is not released after the interaction triggered by the
back
callback. - The object is a detached DOM element or an unmounted React Fiber node.
The other objects allocated by the action
callback could be caches retained
by the web app on purpose, so by default memlab will not report them as leaks.
Filter Out Large Objects
One rule we often find useful is to check for unreleased objects with non-trivial sizes (for example 1MB).
memlab provides an additional leakFilter
callback to filter
out leaked objects with self-defined rules.
/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * @nolint * @oncall web_perf_infra */function url() { return 'http://localhost:3000/';}// action where you suspect the memory leak might be happeningasync function action(page) { await page.click('a[href="/examples/oversized-object"]');}// how to go back to the state before actionwasync function back(page) { await page.click('a[href="/"]');}// leakFilter is called with each object (node) in browser// allocated by `action` but not released after the `back` callfunction leakFilter(node, _snapshot, _leakedNodeIds) { return node.retainedSize > 1000 * 1000;}module.exports = {action, back, leakFilter, url};
Now let's rerun memlab with the updated scenario file:
memlab run --scenario ~/memlab/scenarios/oversized-object.js
By examining the leak trace, we can see that the elements
in bigArray
are
being retained because bigArray
is retained by the eventHandler
closure which is in turn retained because it is still registered as the handler
for an EventListener
. If we add a cleanup function to our useEffect
,
as demonstrated below, and run the scenario again, we will see that there are
no longer any leaks reported by memlab.
useEffect(() => {
window.addEventListener('custom-click', eventHandler);
return () => {
// clean up
window.removeEventListener('custom-click', eventHandler);
};
}, []);
note
An alternative is creating a leak-filter.js
and passing it to memlab find-leaks
if you have run memlab run
already.
/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * @nolint * @oncall web_perf_infra */// leakFilter is called with each object (node) in browser// allocated by `action` but not released after the `back` callfunction leakFilter(node, _snapshot, _leakedNodeIds) { return node.retainedSize > 1000 * 1000;}module.exports = {leakFilter};
Break out the leakFilter
function and save it in a file,
for example, ~/memlab/leak-filters/leak-filter-by-retained-size.js
memlab find-leaks --leak-filter ~/memlab/leak-filters/leak-filter-by-retained-size.js
If you need more advanced filtering logic, here are more examples.