Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add memory leak job #100

Merged
merged 20 commits into from
Jul 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .github/actions/memory-leak/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Run JupyterLab Memory Leak Tests
description: |
Run the JupyterLab memory leak scenarios.

inputs:
# Mandatory inputs
github_token:
description: "The GitHub token to use, must have pull_request:write access"
required: true

# Optional inputs
samples:
description: "Number of samples to compute"
required: false
default: "7"
server_url:
description: The server URL to wait for (see https://github.com/iFaxity/wait-on-action `resource`)
required: false
default: http-get://localhost:8888
git_username:
description: The Git Username to Use for the Commit
required: false
default: github-actions[bot]
git_email:
description: The Git Email to Use for the Commit
required: false
default: github-actions[bot]@users.noreply.github.com

runs:
using: composite
steps:
- name: Install fuite
shell: bash
run: |
set -ex
npm install
working-directory: ${{ github.action_path }}/../../../memory-leaks

- name: Wait for the server
uses: ifaxity/wait-on-action@v1
with:
resource: ${{ inputs.server_url }}
timeout: 360000

- name: Execute memory leaks test
shell: bash
env:
# How many samples to compute
MEMORY_LEAK_NSAMPLES: ${{ inputs.samples }}
run: |
set -ex
npm run test:mocha | tee /tmp/memory-leaks.log
working-directory: ${{ github.action_path }}/../../../memory-leaks

- name: Set job summary
if: always()
shell: bash
run: sed "s/^[[:space:]]*[[:digit:]])[[:space:]].*$//" /tmp/memory-leaks.log >> $GITHUB_STEP_SUMMARY
27 changes: 27 additions & 0 deletions .github/workflows/memory-leak.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: JupyterLab Memory Leak Tests

on:
workflow_dispatch:
inputs:
repository:
description: "JupyterLab Git repository (format {owner}/{repo})"
required: true
type: string
branch:
description: "Git branch to test"
required: true
type: string
samples:
description: "Number of samples to compute"
required: false
default: "7"
type: string

jobs:
memory-leak-test:
# uses: jupyterlab/benchmarks/.github/workflows/run-memory-leak.yml@master
uses: ./.github/workflows/run-memory-leak.yml
with:
repository: ${{ github.event.inputs.repository || 'jupyterlab/jupyterlab' }}
branch: ${{ github.event.inputs.branch || 'master' }}
samples: ${{ github.event.inputs.samples || '7' }}
102 changes: 102 additions & 0 deletions .github/workflows/run-memory-leak.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
name: Run JupyterLab Memory Leak Tests

on:
workflow_call:
inputs:
repository:
description: "JupyterLab Git repository (format {owner}/{repo})"
required: true
type: string
branch:
description: "Git branch to test"
required: true
type: string
samples:
description: "Number of samples to compute"
required: false
default: "7"
type: string

jobs:
memory-leak-test:
runs-on: ubuntu-20.04

env:
# How many samples to compute
MEMORY_LEAK_NSAMPLES: ${{ inputs.samples }}

steps:
- name: Checkout benchmarks project
uses: actions/checkout@v2
with:
path: benchmarks

- name: Install node
uses: actions/setup-node@v2
with:
node-version: "14.x"

- name: Install Python
uses: actions/setup-python@v2
with:
python-version: 3.8

- name: Cache pip on Linux
uses: actions/cache@v1
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-3.8-${{ hashFiles('**/requirements.txt', 'setup.cfg') }}
restore-keys: |
${{ runner.os }}-pip-3.8

- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Cache yarn
uses: actions/cache@v1
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-

- name: Checkout JupyterLab
uses: actions/checkout@v2
with:
repository: ${{ inputs.repository }}
ref: ${{ inputs.branch }}
path: jupyterlab-repo

- name: Install dependencies
run: |
set -ex
bash ./scripts/ci_install.sh
# Build dev mode
jlpm run build
working-directory: jupyterlab-repo

- name: Launch JupyterLab
shell: bash
run: |
# Mount a volume to overwrite the server configuration
jlpm start-jlab 2>&1 > /tmp/jupyterlab_server.log &
working-directory: benchmarks/memory-leaks

- name: Execute memory leaks test
uses: ./benchmarks/.github/actions/memory-leak
with:
github_token: "FAKE"
server_url: "http-get://localhost:9999/lab"

- name: Kill the server
if: always()
shell: bash
run: |
kill -s SIGKILL $(pgrep jupyter-lab)

- name: Print JupyterLab logs
if: always()
run: |
cat /tmp/jupyterlab_server.log

14 changes: 14 additions & 0 deletions .github/workflows/scheduled-memory-leak.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: JupyterLab Weekly Memory Leak Tests

on:
schedule:
# Every Sunday at 01:42am
- cron: "42 1 * * 0"

jobs:
memory-leak-test:
# uses: jupyterlab/benchmarks/.github/workflows/run-memory-leak.yml@master
uses: ./.github/workflows/run-memory-leak.yml
with:
repository: jupyterlab/jupyterlab
branch: master
7 changes: 6 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ jobs:
- name: Install dependencies
run: |
set -ex
echo "OLD_REF_SHA=$(git log -n1 --format='%H')" >> $GITHUB_ENV
bash ./scripts/ci_install.sh
# Build dev mode
jlpm run build
Expand Down Expand Up @@ -127,3 +126,9 @@ jobs:
run: |
cat /tmp/jupyterlab_server.log

memory-leak-test:
# uses: jupyterlab/benchmarks/.github/workflows/run-memory-leak.yml@master
uses: ./.github/workflows/run-memory-leak.yml
with:
repository: jupyterlab/jupyterlab
branch: master
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ It tracks with benchmarks tooling the performance evolution of [JupyterLab](http

## Quickstart

The best way to use this project for benchmark test execution is to start a manual benchmark workflow in the [repository actions](https://github.com/jupyterlab/benchmarks/actions/workflows/benchmark.yml). It will measure the execution of the following scenario:
The best way to use this project for benchmark test execution is to start a manual benchmark workflow in the repository actions for [performance](https://github.com/jupyterlab/benchmarks/actions/workflows/benchmark.yml) or [memory-leaks](https://github.com/jupyterlab/benchmarks/actions/workflows/benchmark.yml).


### Performance tests

The _performance_ tests will measure the execution of the following scenario:

- Opening the test notebook
- Switching from the test notebook to a copy of it
Expand All @@ -35,7 +40,23 @@ The workflow parameters are:
The test notebooks to execute; the available test notebooks are: ["codeNotebook", "mdNotebook", "largePlotly", "longOutput", "manyPlotly", "manyOutputs", "errorOutputs"]
- Test files size [default: 100]: tests notebooks are parametrized with an integer that is proportional to their size.

> You need to remember that a GitHub job is limited to 6hours. This means you may need to either reduce the number of samples (be careful) or the list of test notebooks to fit that time span.
> You need to remember that a GitHub job is limited to 6 hours. This means you may need to either reduce the number of samples (be careful) or the list of test notebooks to fit that time span.

### Memory leaks

The following scenarios are tested for memory leaks:

- notebook: Create a new notebook and delete it.
- file-editor: Create a new text file.
- cell:
- Add a cell (for all 3 types)
- Move a cell by drag-and-drop (for all 3 types)

The workflow parameters are:

- JupyterLab Git repository [required]: fork name in format _{owner}/{repo}_
- Git repository branch [required]: typically branch name of a PR
- Number of samples [default: 7]: Number of experiments to run to detect memory leaks (prefer a prime number).

## License and notice

Expand Down
9 changes: 9 additions & 0 deletions docs/source/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ benchmarks/index
benchmarks/ci
```

```{toctree}
---
maxdepth: 2
caption: Memory Leaks
---
memory-leak/index
memory-leak/ci
```

```{toctree}
---
maxdepth: 2
Expand Down
16 changes: 16 additions & 0 deletions docs/source/memory-leak/ci.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Memory leak on CI

You need to start a manual benchmark workflow in the [repository actions](https://github.com/jupyterlab/benchmarks/actions/workflows/memory-leak.yml).
It will execute the following scenarios for memory leaks:

- notebook: Create a new notebook and delete it.
- file-editor: Create a new text file.
- cell:
- Add a cell (for all 3 types)
- Move a cell by drag-and-drop (for all 3 types)

The workflow parameters are:

- JupyterLab Git repository [required]: fork name in format _{owner}/{repo}_
- Git repository branch [required]: typically branch name of a PR
- Number of samples [default: 7]: Number of experiments to run to detect memory leaks (prefer a prime number).
80 changes: 80 additions & 0 deletions docs/source/memory-leak/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Memory Leak Tooling

This package is using [fuite](https://github.com/nolanlawson/fuite) to execute a memory leak scenarios.

> Under the hood _fuite_ uses [Puppeteer](https://pptr.dev/) to control a Chrome instance. So scenarios must use Puppeteer API.

## Running test

To run the test, you will need to

1. Start JupyterLab after compiling it at a reference state with the following command

```
jupyter lab --config memory-leaks/jupyter_lab_config.py
```

2. Run the tests against the reference

```
cd memory-leaks
yarn install
yarn build
yarn test:mocha
```

3. Stop JupyterLab

The benchmark scenarios are:

- notebook: Create a new notebook and delete it (memory-leaks/tests/notebook.mjs).
- file-editor: Create a new text file (memory-leaks/tests/file-editor.mjs).
- cell:
- Add a cell (for all 3 types) (memory-leaks/tests/cell.mjs)
- Move a cell by drag-and-drop (for all 3 types) (memory-leaks/tests/cell-motion.mjs)

Each _fuite_ scenario is stored in a specific file and can be run separately
using:

```sh
npx fuite http://localhost:9999/lab?reset -s memory-leaks/tests/notebook.mjs
```

You can run in headless mode with the `-d` flag. And you can change the number
of iteration with `-i <number>` option.

> Don't forget the `?reset` query arguments to ensure the workspace is reset between each iteration.

## Finding leaks

From the direct report of `fuite`, you may have stack trace to look at for growing collections. But
may not be sufficient to find all leaks. For that, you can use the
[three heap snapshot technique](https://docs.google.com/presentation/d/1wUVmf78gG-ra5aOxvTfYdiLkdGaR9OhXRnOlIcEmu2s)
introduced by the GMail team. The steps to carry out are:

1. Take a heap snapshot
2. Do some actions that are creating memory leaks; e.g. opening and closing a new notebook.
3. Take a second heap snapshot
4. Do the same actions as in step _2_; e.g. opening and closing a new notebook.
5. Take a third heap snapshot
6. Assuming you are using Chrome-based browser, filter the objects to those allocated between
snapshots 1 and 2 in snapshot 3's _Summary_ view.

This will grant you access to objects leaking in memory. From there:

- Examine the retaining path of leaked objects in the lower half of the Profiles panel

- If the allocation site cannot be easily inferred (i.e. event listeners):
- Instrument the constructor of the retaining object via the JS console to save the stack trace for allocations
Gmail team proposed the following snippet:
```js
window.__save_el = goog.events.Listener
goog.events.Listener = function () { this._stack = new Error().stack;}
goog.events.Listener.prototype = window.__save_el.prototype
```
- Rerun the scenario
- Expand objects in the retaining tree to see the stack trace: `$0._stack`

- Fix it!
- Properly dispose of the retaining object
- unlisten() to event listeners
4 changes: 4 additions & 0 deletions memory-leaks/.mocharc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"timeout": 1200000,
"setup": "./tests/setup.mjs"
}
Loading