Coder Social home page Coder Social logo

fuite's Introduction

fuite

fuite /fɥit/ French for "leak"

fuite is a CLI tool for finding memory leaks in web apps.

Introductory blog post

Tutorial video

Usage

npx fuite https://example.com

This will check for leaks and print output to stdout.

By default, fuite will assume that the site is a client-rendered webapp, and it will search for internal links on the given page. Then for each link, it will:

  1. Click the link
  2. Press the browser back button
  3. Repeat to see if the scenario is leaking

For other scenarios, see scenarios.

How it works

fuite launches Chrome using Puppeteer, loads a web page, and runs a scenario against it. It runs the scenario some number of iterations (7 by default) and looks for objects that leaked 7 times (or 14 times, or 28 times). This might sound like a strange approach, but it's useful for cutting through the noise in memory analysis.

fuite looks for the following leaks:

  • Objects (captured with Chrome heap snapshots)
  • Event listeners
  • DOM nodes (attached to the DOM – detached nodes will show under "Objects")
  • Collections such as Arrays, Maps, Sets, and plain Objects

The default scenario clicks internal links because it's the most generic scenario that can be run against a wide variety of SPAs, and it will often catch leaks if client-side routing is used.

Options

Usage: fuite [options] <url>

Arguments:
  url                        URL to load in the browser and analyze

Options:
  -o, --output <file>        Write JSON output to a file
  -i, --iterations <number>  Number of iterations (default: 7)
  -s, --scenario <scenario>  Scenario file to run
  -S, --setup <setup>        Setup function to run
  -H, --heapsnapshot         Save heapsnapshot files
  -d, --debug                Run in debug mode
  -p, --progress             Show progress spinner (use --no-progress to disable)
  -b, --browser-arg <arg>    Arg(s) to pass when launching the browser
  -V, --version              output the version number
  -h, --help                 display help for command

URL

fuite <url>

The URL to load. This should be whatever landing page you want to start at. Note that you can use --setup for a custom setup function (e.g. to log in with a username/password).

Output

-o, --output <file>

fuite generates a lot of data, but not all of it is shown in the CLI output. To dig deeper, use the --output option to create a JSON file containing fuite's analysis. This contains additional information such as the line of code that an event listener was declared on.

Anything that you see in the CLI, you can also find in the output JSON file.

Iterations

-i, --iterations <number>

By default, fuite runs 7 iterations. But you can change this number.

Why 7? Well, it's a nice, small, prime number. If you repeat an action 7 times and some object is leaking exactly 7 times, it's pretty unlikely to be unrelated. That said, on a very complex page, there may be enough noise that 7 is too small to cut through the noise – so you might try 13, 17, or 19 instead. Or 1 if you like to live dangerously.

Scenario

--scenario <scenario>

The default scenario is to find all internal links on the page, click them, and press the back button. You can also define a scenario file that does whatever you want:

fuite --scenario ./myScenario.mjs https://example.com

Your myScenario.mjs can export several async functions, most of which are optional.

Here is a template:

// myScenario.mjs

/**
 * OPTIONAL: Setup code to run before each test
 * @param { import("puppeteer").Page } page
*/
export async function setup(page) {
}

/**
 * OPTIONAL: Code to run once on the page to determine which tests to run
 * @param { import("puppeteer").Page } page
 */
export async function createTests(page) {
}

/**
 * REQUIRED: Run a single iteration against a page – e.g., click a link and then go back
 * @param { import("puppeteer").Page } page
 * @param { any } data
 */
export async function iteration(page, data) {
}

/**
 * OPTIONAL: Teardown code to run after each test
 * @param { import("puppeteer").Page } page
 */
export async function teardown(page) {
}

/**
 * OPTIONAL: Code to wait asynchronously for the page to become idle
 * @param { import("puppeteer").Page } page
 */
export async function waitForIdle(page) {
}

You can delete any optional functions you don't need.

Note that your scenario file can also extend the default scenario.

setup function (optional)

The async setup function takes a Puppeteer Page as input and returns undefined. It runs before each iteration, or before createTests. This is a good place to log in, if your webapp requires a login.

If this function is not defined, then no setup code will be run.

Note that there is also a --setup flag. If defined, it will override the setup function defined in a scenario.

createTests function (optional)

The async createTests function takes a Puppeteer Page as input and returns an array of test data objects representing the tests to run, and the data to pass for each one. This is useful if you want to dynamically determine what tests to run against a page (for instance, which links to click).

If createTests is not defined, then the default tests are [{}] (a single test with empty data).

The basic shape for a test data object is like so:

{
  "description": "Some human-readable description",
  "data": {
    "some data": "which is passed to the test"
  }
}

For instance, your createTests might return:

[
  {
    "description": "My test 1",
    "data": { "foo": "foo" }
  },
  {
    "description": "My test 2",
    "data": { "foo": "bar" }
  }
]

iteration function (required)

The async iteration function takes a Puppeteer Page and iteration data as input and returns undefined. It runs for each iteration of the memory leak test. The iteration data is a plain object and comes from the createTests function, so by default it is just an empty object: {}.

Inside of an iteration, you want to run the core test logic that you want to test for leaks. The idea is that, at the beginning of the iteration and at the end, the memory should be the same. So an iteration might do things like:

  • Click a link, then go back
  • Click to launch a modal dialog, then press the Esc key
  • Hover to show a tooltip, then hover away to dismiss the tooltip
  • Etc.

The iteration assumes that whatever page it starts at, it ends up at that same page. If you test a multi-page app in this way, then it's extremely unlikely you'll detect any leaks, since multi-page apps don't leak memory in the same way that SPAs do when navigating between routes.

teardown function (optional)

The async teardown function takes a Puppeteer Page as input and returns undefined. It runs after each iteration, or after createTests.

If this function is not defined, then no teardown code will be run.

waitForIdle function (optional)

The async waitForIdle function takes a Puppeteer Page and should resolve when the page is considered "idle."

Here is an example idle check:

export async function waitForIdle(page) {
  await new Promise(resolve => setTimeout(resolve, 2000)) // wait 2 seconds
  await page.waitForSelector('#my-element') // wait for element
}

If this function is not defined, then the default idle check is used. The default is based on heuristics, using the network idle and main thread idle.

Setup

--setup <setup>

The --setup option can define a custom setup function, which runs immediately after the page is loaded, but before any other scenario code.

For instance, you can use --setup to log in with a username/password. To do so, first create a file called mySetup.mjs:

export async function setup (page) {
  await page.type('#username', 'myusername');
  await page.type('#password', 'mypassword');
  await page.click('#submit');
}

Then pass it in:

npx fuite https://example.com --setup ./mySetup.mjs

The setup function defined here is the same one that you can define in a custom scenario using --scenario (i.e. it takes a Puppeteer Page as input).

If both --scenario and --setup are defined, then --setup will override the setup function in the scenario.

Heap snapshot

  -H, --heapsnapshot         Save heapsnapshot files

By default, fuite doesn't save any heap snapshot files that it captures (to avoid filling up your disk with large files). If you use the --heapsnapshot flag, though, then the files will be saved in the /tmp directory, and the CLI will output their location. That way, you can inspect them and load them into the Chrome DevTools memory tool yourself.

Debug

  -d, --debug                Run in debug mode

Debug mode lets you drill in to a complex scenario and debug it yourself using the Chrome DevTools. The best way to run it is:

NODE_OPTIONS=--inspect-brk fuite --debug <url>

(Note that NODE_OPTIONS will not work if you run npx fuite. So you have to install fuite locally or globally, e.g. using npm i -g fuite.)

Then navigate to chrome:inspect in Chrome, click "Open dedicated DevTools for Node," and now you are debugging fuite itself.

This will launch Chrome in non-headless mode, and it will also automatically pause before running iterations and afterwards. That way, you can open up the Chrome DevTools and analyze the scenario yourself, take your own heap snapshots, etc.

Progress

-p, --progress             Show progress spinner (use --no-progress to disable)

Enable or disable the progress spinner while the test runs. It's true by default, so you should use --no-progress to disable.

Browser args

-b, --browser-arg <arg>   Arg(s) to pass when launching the browser

This allows you to pass args (aka flags) into Puppeteer when launching the browser. You can define multiple args, and they are passed ver batim to Puppeteer's launch args option.

For example:

fuite <url> -b --use-fake-device-for-media-stream -b --enable-experimental-web-platform-features

JavaScript API

fuite can also be used via a JavaScript API, which works similarly to the CLI:

import { findLeaks } from 'fuite';

const results = findLeaks('https://example.com', {
  scenario: scenarioObject,
  iterations: 7,
  heapsnapshot: false,
  debug: false,
  progress: true,
  browserArgs: ['--use-fake-device-for-media-stream']
});
for await (const result of results) {
  console.log(result);
}

Note that findLeaks returns an async iterable.

This returns the same output you would get using --output <filename> in the CLI – a plain object describing the leak. The format of the object is not fully specified yet, but a basic shape can be found in the TypeScript types.

Options

The options for findLeaks are basically the same as for the CLI. The only differences between the JavaScript API and the CLI are:

Cancel the test

You can pass in an AbortSignal as the signal option to cancel the test on-demand:

const controller = new AbortController();
const { signal } = controller;
findLeaks('https://example.com', { signal });

// Later
controller.abort();

Scenario object

For the JavaScript API, you can pass in a custom scenario as a plain object. First, define it:

const myScenario = {
  async setup(page) { /* ... */ },
  async createTests(page) { /* ... */ },
  async iteration(page, data) { /* ... */ },
  async teardown(page) { /* ... */ }
};

Then pass it in:

import { findLeaks } from 'fuite';

for await (const result of findLeaks('https://example.com', {
  scenario: myScenario
})) {
  console.log(result);
}

If scenario is undefined, then the default scenario will be used.

Extending the default scenario

If you're writing your own custom scenario, you can also extend the default scenario. For instance, if you want the default scenario, but to be able to log in with a username and password first:

import { defaultScenario, findLeaks } from 'fuite';

const myScenario = {
  ...defaultScenario,
  async setup(page) {
    await page.type('#username', 'myusername')
    await page.type('#password', 'mypassword')
    await page.click('#submit')
  }
};

for await (const result of findLeaks('https://example.com', {
  scenario: myScenario
})) {
  console.log(result);
}

Note that the above works if you're using the JavaScript API. For the CLI, you probably want to use the --setup flag.

Limitations

fuite focuses on the main frame of a page. If you have memory leaks in cross-origin iframes or web workers, then the tool will not find those.

Similarly, fuite measures the JavaScript heap size of the page, corresponding to what you see in the Chrome DevTool's Memory tab. It ignores the size of native browser objects.

fuite works best when your source code is unminified. Otherwise, the class names will show as the minified versions, which can be hard to debug.

fuite may use a lot of memory itself to analyze large heap snapshot files. If you find that Node.js is running out of memory, you can run something like:

NODE_OPTIONS=--max-old-space-size=8000 fuite <url>

The above command will provide 8GB of memory to fuite. (Note that NODE_OPTIONS will not work if you run npx fuite; you have to run fuite directly, e.g. by running npm i -g fuite first.)

FAQs

The results seem wrong or inconsistent.

Try running with --iterations 13 or --iterations 17. The default of 7 iterations is decent, but it might report some false positives.

It says I'm leaking 1kB. Do I really need to fix this?

Not every memory leak is a serious problem. If you're only leaking a few kBs on every interaction, then the user will probably never notice, and you'll certainly never hit an Out Of Memory error in the browser. Your ceiling for "acceptable leaks" will differ, though, depending on your use case. E.g., if you're building for embedded devices, then you probably want to keep your memory usage much lower.

It says my page's memory grew, but it also said it didn't detect any leaks. Why?

Web pages can grow memory for lots of reasons. For instance, the browser's JavaScript engine may JIT certain functions, taking up additional memory. Or the browser may decide to use certain internal data structures to prioritize CPU over memory usage.

The web developer generally doesn't have control over such things, so fuite tries to distinguish between browser-internal memory and JavaScript objects that the page owns. fuite will only say "leak detected" if it can actually give some actionable advice to the web developer.

How do I debug leaking event listeners?

Use the --output command to output a JSON file, which will contain a list of event listeners and the line of code they were declared on. Otherwise, you can use the Chrome DevTools to analyze event listeners:

  • Open the DevTools
  • Open the Elements panel
  • Open Event Listeners
  • Alternatively, run getEventListeners(node) in the DevTools console

How do I debug leaking collections?

fuite will analyze your leaking collections and print out a stacktrace of which code caused the increase – for instance, pushing to an Array, or seting a Map. So this is the first place to look.

If you have sourcemaps, it will show the original source. Otherwise, it'll show the raw stacktrace.

Sometimes more than one thing is increasing the size, and not every increase is at fault (e.g. it deletes right after). In those cases, you should use --output and look at the JSON output to see the full list of stacktraces.

In some other cases, fuite is not able to track increases in collections. (E.g. the object disallows modifications, or the code uses Array.prototype.push.call() instead of .push()ing directly.)

In those cases, you may have to do a manual analysis. Below is how you can do that.

First, run fuite in debug mode:

NODE_OPTIONS=--inspect-brk fuite https://example.com --debug

Then open chrome:inspect in Chrome and click "Open dedicated DevTools for Node." Then, when the breakpoint is hit, open the DevTools in Chromium (the one running your website) and click the "Play" button to let the scenario keep running.

Eventually fuite will give you a breakpoint in the Chrome DevTools itself, where you have access to the leaking collection (Array, Map, etc.) and can inspect it.

It will also give you debugger breakpoints on when the collection is increasing (e.g. push, set, etc.). For plain objects, it tries to override the prototype and spy on setters to accomplish this.

Note that not every leaking collection is a serious memory leak: for instance, your router may keep some metadata about past routes in an ever-growing stack. Or your analytics library may store some timings in an array that continually grows. These are generally not a concern unless the objects are huge, or contain closures that reference lots of memory.

Why not support multiple browsers?

Currently fuite requires Chromium-specific tools such as heap snapshots, getEventListeners, queryObjects, and other things that are only available with Chromium and the Chrome DevTools Protocol (CDP). Potentially, such things could be accessible in a cross-browser way, but today it just isn't possible.

That said, if something is leaking in Chrome, it's likely leaking in Safari and Firefox too.

fuite's People

Contributors

fcollonval avatar nolanlawson avatar stavares843 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fuite's Issues

--login option

The --setup flag is good, but I wonder if something like --login username,password makes even more sense. Probably 99% of people just need to bypass a login screen and start testing their app.

Then again, there are security issues here (passwords in plain text), and maybe I'm better off teaching people how to fish rather than trying to give them a ready-made solution that may not work out-of-the-box (e.g. 2FA, or if there's an additional confirmation screen after the login screen).

Fuite crashing unexpectedly as if any exceptions from toString() are not handled

Hi,
Firstly, thanks for this amazing tool. This has been helpful to accelerate our memory leak testing!

We are facing an issue and below are the details. It would really help us if you can handle this. Or if you can give us permissions, we can raise the pull request as well.

Issue:
Fuite crashes unexpectedly while collecting the heap metrics.

Root cause:

  • In one of our object classes, toString() throws 'NotImplemented' error. This is a legacy code and we dont want to modify this for now.
  • But because of this exception below method in collections.js throws unhandled exception and the Fuite crashes. This is completely blocking our testing.

function isObject (o) { return toString.call(o) === '[object Object]' }

Possible fix:
Handle this in a try catch just like its already handled in getSize() method in same file

        function isObject (o) {
          try {
            return toString.call(o) === '[object Object]'
          } catch (error) {
          // If for whatever reason the tostring() errors (e.g. tostring throws notImplemented exception), return false.
          // Handling this gracefully to avoid Fuite crashes itself
           return false 
          }
        }

During extra iteration for analysis: JavaScript heap out of memory

Hi,

I'm using your awesome tool to find out memory leaks in my project. The whole setup works fine across many different locations in my app. However, it fails on this one with fatal error, somewhat ironically with heap out of memory.

I do have leaks in my app, but they are mostly ~2MB. This one seems to be 4GB and happens on "Extra iteration for analysis" which makes me think that it may be not directly related to my app. The memory usage is pretty much constant even if I do 17 iterations, but it blows up on the extra.

I'm didn't really have much luck with minimal reproduction.

npx fuite --scenario=tests/fuite/scenario.mjs --iterations=1 http://localhost:5173/login

URL       : http://localhost:5173/login
Scenario  : tests/fuite/scenario.mjs
Iterations: 1

TEST RESULTS

--------------------

⠧ Gathering tests...[ '/org/OrgA/payroll' ]
⠦ Test 1/1 - Go to /org/OrgA/payroll and back - Extra iteration for analysis...
<--- Last few GCs --->

[598257:0x6a4ec80]    40692 ms: Scavenge 4024.6 (4110.5) -> 4022.0 (4111.8) MB, 6.20 / 0.00 ms  (average mu = 0.913, current mu = 0.700) allocation failure; 
[598257:0x6a4ec80]    40715 ms: Scavenge 4027.1 (4112.8) -> 4024.6 (4131.8) MB, 7.64 / 0.00 ms  (average mu = 0.913, current mu = 0.700) allocation failure; 
[598257:0x6a4ec80]    40994 ms: Mark-Compact 4042.4 (4135.8) -> 4035.9 (4145.6) MB, 223.42 / 0.00 ms  (average mu = 0.821, current mu = 0.523) allocation failure; scavenge might not succeed


<--- JS stacktrace --->

FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
 1: 0xc9e850 node::Abort() [node]
 2: 0xb720ff  [node]
 3: 0xec1a70 v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [node]
 4: 0xec1d57 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [node]
 5: 0x10d3dc5  [node]
 6: 0x10ebc48 v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [node]
 7: 0x10c1d61 v8::internal::HeapAllocator::AllocateRawWithLightRetrySlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [node]
 8: 0x10c2ef5 v8::internal::HeapAllocator::AllocateRawWithRetryOrFailSlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [node]
 9: 0x109f536 v8::internal::Factory::AllocateRaw(int, v8::internal::AllocationType, v8::internal::AllocationAlignment) [node]
10: 0x1091024 v8::internal::FactoryBase<v8::internal::Factory>::AllocateRawWithImmortalMap(int, v8::internal::AllocationType, v8::internal::Map, v8::internal::AllocationAlignment) [node]
11: 0x10938a6 v8::internal::FactoryBase<v8::internal::Factory>::NewRawOneByteString(int, v8::internal::AllocationType) [node]
12: 0x12052a5 v8::internal::JsonParser<unsigned short>::MakeString(v8::internal::JsonString const&, v8::internal::Handle<v8::internal::String>) [node]
13: 0x120b2bd  [node]
14: 0x120d229 v8::internal::JsonParser<unsigned short>::ParseJson(v8::internal::Handle<v8::internal::Object>) [node]
15: 0xf537fe v8::internal::Builtin_JsonParse(int, unsigned long*, v8::internal::Isolate*) [node]
16: 0x7f75c3699df6 
Aborted (core dumped)

Any idea what it may be?

One thing I've noticed is that output.json from other pages is 20MB in size, mostly due to listing indexes of DOM nodes. Is that expected?

Thanks for help.

JS API: debug setting not respected

It seems that using the debug setting doesn't work properly with the JS API, as Puppeteer is closed immediately after the iterations end.

Here's my fuite.mjs file:

import { defaultScenario, findLeaks } from 'fuite'
import { createTests, setup, iteration } from './conversationsToSettings.scenario.mjs'

const myScenario = {
  ...defaultScenario,
  createTests,
  setup,
  iteration
}

let log = []

for await (const result of findLeaks('https://localhost:8888', {
  scenario: myScenario,
  iterations: 7,
  heapsnapshot: false,
  debug: process.env.DEBUG || false,
  progress: true,
})) {
  const entry = JSON.stringify(result, null, 2)
  // console.log(entry)
  log.push(entry)
}

And I'm running it like this

DEBUG=true node fuite.mjs

How to create an isolated navigation scenario?

Hi,

I can't seem to figure out how could I tell fuite to only perform a single navigation in the whole test suite? The default iteration logic suits me just fine, but when I define a single link in my own custom createTests:

export async function createTests(_page) {
  return [
    {
      data: {
        href: `/settings/profile/general`,
        fullHref: `https://localhost:8888/settings/profile/general`,
      },
      description: `Go to Profile settings and back`
    }
  ]
}

Then fuite just exits with an empty error:

{
    "test": {
        "data": {
            "href": "/settings/profile/general",
            "fullHref": "https://localhost:8888/settings/profile/general"
        },
        "description": "Go to Profile settings and back"
    },
    "result": {
        "failed": true,
        "error": {}
    }
}

This would be useful for when I've already detected a leaking scenario manually and would like to use Fuite less as a discovery tool but more for its friendly debugging/logs purposes.

Detect what code is causing a collection to leak

For Arrays, Maps, and Sets, it should be possible to override functions that append to it and then capture stacktraces for when these functions are called. For plain Objects I'm not sure.

Right now I document this in the readme as a manual step, but it'd be nice to automate it...

Element a[href="/cookies"] is not clickable or is not an in-page SPA navigation

Hi, while trying the tool with an Angular application, I get some errors about links that cannot be clicked.

Here's the error:

Test         : Go to /cookies and back
Failed       : Element a[href="/cookies"] is not clickable or is not an in-page SPA navigation
Node is either not clickable or not an HTMLElement
Error: Element a[href="/cookies"] is not clickable or is not an in-page SPA navigation
Node is either not clickable or not an HTMLElement
    at clickFirstVisible (file:///C:/Users/Fabian/AppData/Roaming/npm-cache/_npx/9228/node_modules/fuite/src/defaultScenario.js:35:13)
    at runMicrotasks (<anonymous>)
    at processTicksAndRejections (internal/process/task_queues.js:95:5)
    at async iteration (file:///C:/Users/Fabian/AppData/Roaming/npm-cache/_npx/9228/node_modules/fuite/src/defaultScenario.js:85:3)
    at async file:///C:/Users/Fabian/AppData/Roaming/npm-cache/_npx/9228/node_modules/fuite/src/index.js:94:7
    at async runOnFreshPage (file:///C:/Users/Fabian/AppData/Roaming/npm-cache/_npx/9228/node_modules/fuite/src/index.js:26:13)
    at async runIterationOnPage (file:///C:/Users/Fabian/AppData/Roaming/npm-cache/_npx/9228/node_modules/fuite/src/index.js:93:13)
    at async file:///C:/Users/Fabian/AppData/Roaming/npm-cache/_npx/9228/node_modules/fuite/src/index.js:184:17

Here's the link in the DOM:
image

Error: spawn Unknown system error -86

URL : https://melishev.ru
Scenario : Default
Iterations: 7 (Default)

TEST RESULTS


Error: spawn Unknown system error -86
at ChildProcess.spawn (node:internal/child_process:412:11)
at Object.spawn (node:child_process:698:9)
at BrowserRunner.start (/Users/matvejmelishev/.npm/_npx/4f0bae50eb4a6363/node_modules/puppeteer/lib/cjs/puppeteer/node/BrowserRunner.js:91:34)
at ChromeLauncher.launch (/Users/matvejmelishev/.npm/_npx/4f0bae50eb4a6363/node_modules/puppeteer/lib/cjs/puppeteer/node/Launcher.js:115:16)
at async analyzeOptions (file:///Users/matvejmelishev/.npm/_npx/4f0bae50eb4a6363/node_modules/fuite/src/index.js:34:19)
at async findLeaks (file:///Users/matvejmelishev/.npm/_npx/4f0bae50eb4a6363/node_modules/fuite/src/index.js:96:7)
at async main (file:///Users/matvejmelishev/.npm/_npx/4f0bae50eb4a6363/node_modules/fuite/src/cli.js:87:20) {
errno: -86,
code: 'Unknown system error -86',
syscall: 'spawn'
}

Test mode

I can imagine people wanting to use fuite in their CI tests to exit with a 0 or a 1 based on whether any leaks were detected. The CLI could do this today, but it'd be a breaking change because currently it's always 0 unless an unexpected error occurs.

There would probably need to be a system to help manage false positives, e.g. --maxAllowedLeakingCollections or --maxAllowedLeakingObjects. It could either be a superset of the existing CLI, or a new CLI (e.g. fuite-test).

Or it could be a separate standalone tool. Or something that could be integrated into a test runner. Or maybe just some documentation about how to run the existing tool and output the exit code you want. I dunno.

[Request] Add CLI option to pass chrome flags to puppeteer

First off, kudos on this project. I've been reading your blog posts as they come up on HN and I'm excited to see where you take this.

I'm working on an app that makes heavy use of audio and video inputs and in order to automate this I need to be able to launch chrome with some flags turned on. For now I've just manually updated the launch script to pass the arguments like this:

const browser = await puppeteer.launch({
    headless: !debug,
    defaultViewport: { width: 1280, height: 800 },
    args: [
      '--use-fake-device-for-media-stream',
      '--use-file-for-fake-video-capture\=/path/to/webcam-audio-video-sync.y4m',
      '--use-file-for-fake-audio-capture\=/path/to/webcam-audio-video-sync.wav',
      '--use-fake-ui-for-media-stream',
    ],
  })

It would be useful to be able to pass these through the fuite CLI. Something like fuite http://localhost:8080 -s tests/fuite/login.scenario.mjs --chrome-flags --use-fake-device-for-media-stream etc.

Add heuristics to filter trivial collection leaks

Oftentimes the collections leak analyzer will find a leak in some trivial array of integers or small objects or something due to an analytics library, soft navigation framework, etc. These are usually not worth fixing, but they can be noise when trying to find a real leak in the system.

fuite could automatically detect these kinds of trivial collections leaks using heuristics and then filter them out. You could disable the behavior with a flag like --show-all-leaking-collections or something.

Allow custom "website is idle" logic

fuite has some built-in logic to try to determine that a web page is idle, but this should be customizable because it doesn't always work for every web app.

Support output for JS API

Hello again! 👋

Would you accept a PR to add output as an option to the findLeaks options similarly to what the CLI supports today, so I as a user wouldn't have to handle dumping the output to a JSON file?

Chromium detects object leaks in multi-page apps

For some reason, between Puppeteer 21.7.0 and 21.9.0, the new version of Chromium actually detects leaking objects for multi-page apps. The objects are standard global objects like Document and JSON.

This is very bizarre to me, and potentially confusing for people who might mistakenly point fuite at a multi-page app. I'm not sure what to do about this, if this is a true leak in Chromium or just an issue with how I'm taking snapshots, or what.

// TODO: Chromium started reporting true here https://github.com/nolanlawson/fuite/issues/92
// expect(result.leaks.detected).to.equal(false)

Show retainer chain

The "count the objects" technique is usually pretty good at finding the "head vampire" (root of the retainer chain), but it's not always perfect. fuite could potentially show the actual retainer chain for these objects.

My only hesitation about this is that I feel like it devolves into reimplementing the Chrome DevTools, in particular the UI for diffing heap snapshots, whereas I already have instructions where you can diff your own heap snapshots in --debug mode.

error again v4.0.2 and During extra iteration for analysis: JavaScript heap out of memory

Hi,
similar to #86
despite upgrade to v4.0.2 ,I'm still getting this error。"ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory"

I have studied the latest merged code. b413f5b

node --max-old-space-size=16000 ./node_modules/.bin/fuite   http://localhost:1234/tool --output ./out.json --setup ./mySetup.mjs -s ./myScenario.mjs

---


URL       : http://localhost:1234/tool
Scenario  : ./myScenario.mjs
Iterations: 7 (Default)
Output    : ./out.json

 TEST RESULTS
---------
⠦ Gathering tests...
<--- Last few GCs --->

[62300:0x118028000]   775347 ms: Scavenge 15786.9 (16024.1) -> 15786.7 (16035.6) MB, 18.8 / 0.0 ms  (average mu = 0.928, current mu = 0.733) allocation failure;
[62300:0x118028000]   775389 ms: Scavenge 15795.3 (16036.5) -> 15793.6 (16036.8) MB, 17.8 / 0.0 ms  (average mu = 0.928, current mu = 0.733) allocation failure;
[62300:0x118028000]   778900 ms: Scavenge 15795.5 (16036.8) -> 15793.4 (16058.3) MB, 3506.7 / 0.0 ms  (average mu = 0.928, current mu = 0.733) allocation failure;


<--- JS stacktrace --->

FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
 1: 0x100cb3938 node::Abort() [v18.4.0/bin/node]
 2: 0x100cb3ac0 node::errors::TryCatchScope::~TryCatchScope() [v18.4.0/bin/node]
 3: 0x100e05944 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [v18.4.0/bin/node]
 4: 0x100fb0498 v8::internal::EmbedderStackStateScope::EmbedderStackStateScope(v8::internal::Heap*, v8::internal::EmbedderStackStateScope::Origin, cppgc::EmbedderStackState) [node/v18.4.0/bin/node]
 5: 0x100faef7c v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [v18.4.0/bin/node]
 6: 0x100fa3224 v8::internal::HeapAllocator::AllocateRawWithLightRetrySlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [v18.4.0/bin/node]
 7: 0x100fa3a54 v8::internal::HeapAllocator::AllocateRawWithRetryOrFailSlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [v18.4.0/bin/node]
 8: 0x100f89888 v8::internal::Factory::NewFillerObject(int, v8::internal::AllocationAlignment, v8::internal::AllocationType, v8::internal::AllocationOrigin) [v18.4.0/bin/node]
 9: 0x101319904 v8::internal::Runtime_AllocateInYoungGeneration(int, unsigned long*, v8::internal::Isolate*) [v18.4.0/bin/node]
10: 0x10166502c Builtins_CEntry_Return1_DontSaveFPRegs_ArgvOnStack_NoBuiltinExit [v18.4.0/bin/node]
11: 0x105fe8f18
12: 0x105fba9fc
13: 0x1016871c4 Builtins_ArrayReduceLoopContinuation [v18.4.0/bin/node]
14: 0x101687924 Builtins_ArrayReduce [v18.4.0/bin/node]
15: 0x105fea818
16: 0x105f997e0
17: 0x105f99248
18: 0x105f53e9c
...
[1]    62300 abort      node --max-old-space-size=16000  http://localhost:1234/tool
--output

I do have leaks in my app。
By annotating this function, I obtained the results in the table below. I suspect that the content of the "leak collections array" is too long, causing insufficient default JavaScript execution memory.

fuite/src/index.js

Lines 167 to 178 in a004e33

try {
if (metrics.some(metric => metric.needsExtraIteration?.())) {
onProgress('Extra iteration for analysis...')
await iteration(page, test.data)
for (const metric of metrics) {
await (metric.afterExtraIteration?.())
}
}
} catch (err) {
// ignore if the extra iteration doesn't work for any reason; it's optional
// TODO: error log
}

Leaking collections:

╔════════╤════════╤═════════════════════════════════════╤═══════════════════╗
║ Type   │ Change │ Preview                             │ Size increased at ║
╟────────┼────────┼─────────────────────────────────────┼───────────────────╢
║ Array  │ +1     │ [{id, latency, entries}, ...]       │                   ║
╟────────┼────────┼─────────────────────────────────────┼───────────────────╢
║ Array  │ +301   │ [function init () {}, ...]              │               ║
╟────────┼────────┼─────────────────────────────────────┼───────────────────╢
║ Object │ +1     │ {1023: {id, latency, entries}, ...} │                   ║
╚════════╧════════╧═════════════════════════════════════╧═══════════════════╝

One attempt

I attempted to modify the source code to analyze only the first 50 entries of the array, keeping the other parameters unchanged, and the program was able to run successfully.

return (await promisePool(PROMISE_POOL_SIZE, collections.map(collection => async () => {
    const res = { ...collection }
    if (collection.id in idsToStacktraces) {
      const stacktraces = idsToStacktraces[collection.id].slice(0,50)  // change
      if(idsToStacktraces[collection.id].length>50){      // change
        console.log('yes,you got it');                    // change
      }
      res.stacktraces = await Promise.all(stacktraces.map(async stacktrace => {
        return getStacktraceWithOriginalAndPretty(stacktrace)
      }))
    }
    return res
  })))

Can you give me some advice?

Show allocation stacktraces for arbitrary objects

If we know that class Foo is leaking 1 time per iteration, then we could potentially use queryObjects to find all objects with the Foo constructor, grab its prototype, and modify its prototype to spy on its constructor.

Potential problems with this approach:

  • Especially with minification, multiple objects may have the same constructor name
  • Browser built-in objects may resist modification
  • Do we want to do this for objects that leak 2 times? what about 200 times? Probably there should be a limit.

Output shows floating point numbers when leaks are inconsistent

If a page is leaking nodes / listeners at an irregular rate (e.g. for 7 iterations, a node leaked 9 times), then the CLI will show a floating point number (e.g. 1.2857142857142858 per iteration).

I don't really want to use the same "it must be exactly 7 iterations" logic for DOM nodes or event listeners, because in those cases, a leak is a leak, but maybe the output can be a bit cleaner.

Use sourcemaps for leaking classes in heap snapshots

Rather than say that an object named Zx is leaking, it would be great to show the original name. Now that #25 proves we can have sourcemaps, it seems like it should be possible to extract sourcemaps for heap snapshot class names. (I mean, the DevTools seem to be able to point a retainer chain back to the leaking line of code, so why not?)

I don't know if this is actually possible, though.

How can I analyze the test result?

I used this tool to handle a production project. SPA with dozens of routes, for 35,000 code terms.
I just dropped the main page: npx fuite https://app.com -o res.json.
About timeout (43 links checked, 7 iterations) gave me a res.json file...
But it contains 7,987,017 lines and is about 200 MB in size.

As far as I understand, there is a detailed count for each transition/test.

That's cool, but I'd like to get a simple nameplate:

+------------------------+--------+
| ROUTE                  | LEAK   |
+------------------------+--------+
| https://app.com/about  | 0.3 KB |
| https://app.com/shop   | 10 MB  |
| https://app.com/my     | 0 B    |
+------------------------+--------+

But when I already understand that the transition to `/shop' is problematic, then I already want to learn the smaller subtleties.

Question 0: How can I get such a plate?
Question 1: How can I analyze the data already received? Is there software for this?

Show actual leaking DOM nodes

Right now the output just says "DOM size increased by X", but it would be neat if it could actually show a preview of what the leaking DOM nodes look like (e.g. div#foo).

Leaking DOM timers

It should be possible to override window.setTimeout / window.setInterval to capture when these are leaking. Questions are:

  1. Can it be done in such a way that it works even when people cache the window.setInterval function?
  2. Is it really worth it, given that the heapsnapshot tool lists DOMTimer as one of the leaking classes?

Update Puppeteer to 22 so fuite can run on Linux machines

An issue has come up here with Puppeteer: puppeteer/puppeteer#11967

In effect, the problem is that older versions of Puppeteer, such as 21 which the latest version of fuite depends on, don't work on Linux environments anymore (or at least not consistently) because the URL it specifies to download a chrome runtime from is being phased out.

Updating the version of puppeteer to the latest will fix this problem.

ReferenceError: AbortController is not defined

Fuite sounds great! Unfortunately I can't get it to run - is there a particular Node version required? Here's the error output:

$ npx fuite --debug https://pinafore.social
ReferenceError: AbortController is not defined
    at main (file:///home/vincent/.npm/_npx/4f0bae50eb4a6363/node_modules/fuite/src/cli.js:31:3)
    at file:///home/vincent/.npm/_npx/4f0bae50eb4a6363/node_modules/fuite/src/cli.js:114:1
    at ModuleJob.run (internal/modules/esm/module_job.js:170:25)
    at async Loader.import (internal/modules/esm/loader.js:178:24)

This is using 14.17.5 and npm 7.21.0.

Support websites built with Flutter

When I run fuite to a site built with 'flutter', I only receive 'No tests to run.' messages.

Is there a way to test with the "flutter" site?

ERR_UNSUPPORTED_ESM_URL_SCHEME on Windows

I'm trying to launch this on my Windows 10 / Node 16.13.2 with:

npx fuite http://localhost:4000 --setup .\setup.mjs

or

npx fuite http://localhost:4000 --setup ./setup.mjs

but I get this error:

Error [ERR_UNSUPPORTED_ESM_URL_SCHEME]: Only file and data URLs are supported by the default ESM loader. On Windows, absolute paths must be valid file:// URLs. Received protocol 'c:'
    at new NodeError (node:internal/errors:371:5)
    at defaultResolve (node:internal/modules/esm/resolve:1016:11)
    at ESMLoader.resolve (node:internal/modules/esm/loader:422:30)
    at ESMLoader.getModuleJob (node:internal/modules/esm/loader:222:40)
    at ESMLoader.import (node:internal/modules/esm/loader:276:22)
    at importModuleDynamically (node:internal/modules/esm/translators:111:35)
    at importModuleDynamicallyCallback (node:internal/process/esm_loader:35:14)
    at main (file:///C:/prj/node_modules/fuite/src/cli.js:47:41)
    at file:///C:/prj/node_modules/fuite/src/cli.js:130:1
    at ModuleJob.run (node:internal/modules/esm/module_job:185:25) {
  code: 'ERR_UNSUPPORTED_ESM_URL_SCHEME'
}
  • setup.mjs:
export async function setup(page) {
  await page.type("#email", "[email protected]");
  await page.type("#password", "pass");
  await page.click("submit");
}

Why?

Add a change log

It's always useful to have a change log in the repo. Even if there are no breaking changes, I use them often to gauge if it's worthwhile to make an update.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.