Skip to main content

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

memlab run result

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.