From 37b377826447378841377f3f188737f587d0c308 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Fri, 3 Aug 2018 15:09:38 -0700 Subject: [PATCH 1/9] Install nbresuse by default Extremely useful to see how much RAM you are using right now --- tljh/installer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tljh/installer.py b/tljh/installer.py index 66daf1e..805be18 100644 --- a/tljh/installer.py +++ b/tljh/installer.py @@ -216,7 +216,9 @@ def ensure_user_environment(user_requirements_txt_file): 'jupyterlab==0.32.1', 'nteract-on-jupyter==1.8.1', # nbgitpuller for easily pulling in Git repositories - 'nbgitpuller==0.6.1' + 'nbgitpuller==0.6.1', + # nbresuse to show people how much RAM they are using + 'nbresuse==0.3.0' ]) if user_requirements_txt_file: From 1f4acfead3b901196df597074fbafe50194cfa04 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Sat, 4 Aug 2018 09:58:28 -0700 Subject: [PATCH 2/9] Install gcc by default Required by psutil, which is required by nbresuse --- tljh/installer.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tljh/installer.py b/tljh/installer.py index 805be18..fa2b838 100644 --- a/tljh/installer.py +++ b/tljh/installer.py @@ -203,6 +203,11 @@ def ensure_user_environment(user_requirements_txt_file): with conda.download_miniconda_installer(miniconda_version, miniconda_installer_md5) as installer_path: conda.install_miniconda(installer_path, USER_ENV_PREFIX) + # nbresuse needs psutil, which requires gcc + apt.install_packages([ + 'gcc' + ]) + conda.ensure_conda_packages(USER_ENV_PREFIX, [ # Conda's latest version is on conda much more so than on PyPI. 'conda==4.5.8' From a8e6165fa5af11e19783b8a10bd8e1eaf159cca0 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Sat, 4 Aug 2018 10:11:20 -0700 Subject: [PATCH 3/9] Add test to check if serverextensions are installed --- integration-tests/test_hub.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/integration-tests/test_hub.py b/integration-tests/test_hub.py index a70028a..95cb65c 100644 --- a/integration-tests/test_hub.py +++ b/integration-tests/test_hub.py @@ -2,6 +2,7 @@ import requests from hubtraf.user import User from hubtraf.auth.dummy import login_dummy import secrets +import subprocess import pytest from functools import partial import asyncio @@ -104,4 +105,24 @@ async def test_user_admin_remove(): await u.ensure_server() # Assert that the user does *not* have admin rights - assert f'jupyter-{username}' in grp.getgrnam('jupyterhub-admins').gr_mem \ No newline at end of file + assert f'jupyter-{username}' in grp.getgrnam('jupyterhub-admins').gr_mem + + +def test_serverextensions(): + """ + Validate serverextensions we want are installed + """ + proc = subprocess.run([ + '/opt/tljh/user/bin/jupyter-serverextensions', + 'list', '--sys-prefix' + ], capture_output=True, universal_newlines=True) + + extensions = [ + 'jupyterlab 0.32.1', + 'nbgitpuller 0.6.1', + 'nteract_on_jupyter 1.8.1', + 'nbresuse' + ] + + for e in extensions: + assert '{} ^[[32mOK^[[0m' in e.stderr From fbfa932c21d10e5302b4715c8c4daff246b5298c Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Sat, 4 Aug 2018 10:23:42 -0700 Subject: [PATCH 4/9] Don't use subprocess.run features from 3.7 --- integration-tests/test_hub.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration-tests/test_hub.py b/integration-tests/test_hub.py index 95cb65c..b9f6b7f 100644 --- a/integration-tests/test_hub.py +++ b/integration-tests/test_hub.py @@ -115,7 +115,7 @@ def test_serverextensions(): proc = subprocess.run([ '/opt/tljh/user/bin/jupyter-serverextensions', 'list', '--sys-prefix' - ], capture_output=True, universal_newlines=True) + ], stderr=subprocess.PIPE) extensions = [ 'jupyterlab 0.32.1', @@ -125,4 +125,4 @@ def test_serverextensions(): ] for e in extensions: - assert '{} ^[[32mOK^[[0m' in e.stderr + assert '{} ^[[32mOK^[[0m' in e.stderr.decode() From 710bee593b27d34c17e39ce8c430bbfe4ecd5a30 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Sat, 4 Aug 2018 10:28:34 -0700 Subject: [PATCH 5/9] Fix path to jupyter-serverextensions --- integration-tests/test_hub.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-tests/test_hub.py b/integration-tests/test_hub.py index b9f6b7f..e42254c 100644 --- a/integration-tests/test_hub.py +++ b/integration-tests/test_hub.py @@ -113,7 +113,7 @@ def test_serverextensions(): Validate serverextensions we want are installed """ proc = subprocess.run([ - '/opt/tljh/user/bin/jupyter-serverextensions', + '/opt/tljh/user/bin/jupyter-serverextension', 'list', '--sys-prefix' ], stderr=subprocess.PIPE) From ebc9530442e2ba4211e1c630410b4c5a481ed442 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Sat, 4 Aug 2018 10:34:06 -0700 Subject: [PATCH 6/9] Fixup formatting for serverextensions test --- integration-tests/test_hub.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration-tests/test_hub.py b/integration-tests/test_hub.py index e42254c..a6253a0 100644 --- a/integration-tests/test_hub.py +++ b/integration-tests/test_hub.py @@ -121,8 +121,8 @@ def test_serverextensions(): 'jupyterlab 0.32.1', 'nbgitpuller 0.6.1', 'nteract_on_jupyter 1.8.1', - 'nbresuse' + 'nbresuse ' ] for e in extensions: - assert '{} ^[[32mOK^[[0m' in e.stderr.decode() + assert '{} \x1b[32mOK\x1b[0m'.format(e) in proc.stderr.decode() From 612124f29812c27cdb6f6eab919c58d18a6d760e Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Sat, 4 Aug 2018 10:54:26 -0700 Subject: [PATCH 7/9] Add test for nbextensions too --- integration-tests/test_hub.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/integration-tests/test_hub.py b/integration-tests/test_hub.py index a6253a0..8bfd365 100644 --- a/integration-tests/test_hub.py +++ b/integration-tests/test_hub.py @@ -112,6 +112,7 @@ def test_serverextensions(): """ Validate serverextensions we want are installed """ + # jupyter-serverextension writes to stdout and stderr weirdly proc = subprocess.run([ '/opt/tljh/user/bin/jupyter-serverextension', 'list', '--sys-prefix' @@ -126,3 +127,23 @@ def test_serverextensions(): for e in extensions: assert '{} \x1b[32mOK\x1b[0m'.format(e) in proc.stderr.decode() + +def test_nbextensions(): + """ + Validate nbextensions we want are installed & enabled + """ + # jupyter-nbextension writes to stdout and stderr weirdly + proc = subprocess.run([ + '/opt/tljh/user/bin/jupyter-nbextension', + 'list', '--sys-prefix' + ], stderr=subprocess.PIPE, stdout=subprocess.PIPE) + + extensions = [ + 'nbresuse/main', + ] + + for e in extensions: + assert '{} \x1b[32m enabled \x1b[0m'.format(e) in proc.stdout.decode() + + # Ensure we have 'OK' messages in our stdout, to make sure everything is importable + proc.stderr.decode() == ' - Validating: \x1b[32mOK\x1b[0m\n' * len(extensions) \ No newline at end of file From 907d2b8f5bff116e53b9c2243ae5f2f96c9154ff Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Sat, 4 Aug 2018 10:55:25 -0700 Subject: [PATCH 8/9] Move nb/serverextension tests to own file --- integration-tests/test_extensions.py | 42 ++++++++++++++++++++++++++ integration-tests/test_hub.py | 44 +--------------------------- 2 files changed, 43 insertions(+), 43 deletions(-) create mode 100644 integration-tests/test_extensions.py diff --git a/integration-tests/test_extensions.py b/integration-tests/test_extensions.py new file mode 100644 index 0000000..c57cbef --- /dev/null +++ b/integration-tests/test_extensions.py @@ -0,0 +1,42 @@ +import subprocess + + +def test_serverextensions(): + """ + Validate serverextensions we want are installed + """ + # jupyter-serverextension writes to stdout and stderr weirdly + proc = subprocess.run([ + '/opt/tljh/user/bin/jupyter-serverextension', + 'list', '--sys-prefix' + ], stderr=subprocess.PIPE) + + extensions = [ + 'jupyterlab 0.32.1', + 'nbgitpuller 0.6.1', + 'nteract_on_jupyter 1.8.1', + 'nbresuse ' + ] + + for e in extensions: + assert '{} \x1b[32mOK\x1b[0m'.format(e) in proc.stderr.decode() + +def test_nbextensions(): + """ + Validate nbextensions we want are installed & enabled + """ + # jupyter-nbextension writes to stdout and stderr weirdly + proc = subprocess.run([ + '/opt/tljh/user/bin/jupyter-nbextension', + 'list', '--sys-prefix' + ], stderr=subprocess.PIPE, stdout=subprocess.PIPE) + + extensions = [ + 'nbresuse/main', + ] + + for e in extensions: + assert '{} \x1b[32m enabled \x1b[0m'.format(e) in proc.stdout.decode() + + # Ensure we have 'OK' messages in our stdout, to make sure everything is importable + proc.stderr.decode() == ' - Validating: \x1b[32mOK\x1b[0m\n' * len(extensions) \ No newline at end of file diff --git a/integration-tests/test_hub.py b/integration-tests/test_hub.py index 8bfd365..a70028a 100644 --- a/integration-tests/test_hub.py +++ b/integration-tests/test_hub.py @@ -2,7 +2,6 @@ import requests from hubtraf.user import User from hubtraf.auth.dummy import login_dummy import secrets -import subprocess import pytest from functools import partial import asyncio @@ -105,45 +104,4 @@ async def test_user_admin_remove(): await u.ensure_server() # Assert that the user does *not* have admin rights - assert f'jupyter-{username}' in grp.getgrnam('jupyterhub-admins').gr_mem - - -def test_serverextensions(): - """ - Validate serverextensions we want are installed - """ - # jupyter-serverextension writes to stdout and stderr weirdly - proc = subprocess.run([ - '/opt/tljh/user/bin/jupyter-serverextension', - 'list', '--sys-prefix' - ], stderr=subprocess.PIPE) - - extensions = [ - 'jupyterlab 0.32.1', - 'nbgitpuller 0.6.1', - 'nteract_on_jupyter 1.8.1', - 'nbresuse ' - ] - - for e in extensions: - assert '{} \x1b[32mOK\x1b[0m'.format(e) in proc.stderr.decode() - -def test_nbextensions(): - """ - Validate nbextensions we want are installed & enabled - """ - # jupyter-nbextension writes to stdout and stderr weirdly - proc = subprocess.run([ - '/opt/tljh/user/bin/jupyter-nbextension', - 'list', '--sys-prefix' - ], stderr=subprocess.PIPE, stdout=subprocess.PIPE) - - extensions = [ - 'nbresuse/main', - ] - - for e in extensions: - assert '{} \x1b[32m enabled \x1b[0m'.format(e) in proc.stdout.decode() - - # Ensure we have 'OK' messages in our stdout, to make sure everything is importable - proc.stderr.decode() == ' - Validating: \x1b[32mOK\x1b[0m\n' * len(extensions) \ No newline at end of file + assert f'jupyter-{username}' in grp.getgrnam('jupyterhub-admins').gr_mem \ No newline at end of file From b5be59eae03887612de624b7906418ed90a6f3b2 Mon Sep 17 00:00:00 2001 From: yuvipanda Date: Sun, 5 Aug 2018 09:44:28 -0700 Subject: [PATCH 9/9] Add docs for nbresuse --- docs/howto/nbresuse.rst | 15 +++++++++++++++ docs/howto/resource-estimation.rst | 10 ++++------ docs/images/nbresuse.png | Bin 0 -> 5609 bytes docs/index.rst | 1 + 4 files changed, 20 insertions(+), 6 deletions(-) create mode 100644 docs/howto/nbresuse.rst create mode 100644 docs/images/nbresuse.png diff --git a/docs/howto/nbresuse.rst b/docs/howto/nbresuse.rst new file mode 100644 index 0000000..616146d --- /dev/null +++ b/docs/howto/nbresuse.rst @@ -0,0 +1,15 @@ +.. _howto/nbresuse: + +======================= +Check your memory usage +======================= + +The `nbresuse `_ extension is part of +the default installation, and tells you how much memory your user is using +right now, and what the memory limit for your user is. It is shown in the +top right corner of the notebook interface. Note that this is memory usage +for everything your user is running through the Jupyter notebook interface, +not just the specific notebook it is shown on. + +.. image:: ../images/nbresuse.png + :alt: Memory limit / usage shown with nbresuse diff --git a/docs/howto/resource-estimation.rst b/docs/howto/resource-estimation.rst index cc21d46..d503196 100644 --- a/docs/howto/resource-estimation.rst +++ b/docs/howto/resource-estimation.rst @@ -40,16 +40,14 @@ Maximum memory allowed per user Depending on what kinda work your users are doing, they will use different amounts of memory. The easiest way to determine this is to run through a typical user -workflow yourself, and measure how much memory is used. +workflow yourself, and measure how much memory is used. You can use :ref:`howto/nbresuse` +to determine how much memory your user is using. -For example, you can begin running a Jupyter Notebook session on your JupyterHub, then open a -terminal on the JupyterHub and use ``top`` to track how much memory you use -as you go through the material. A good rule of thumb is to take the maximum amount of memory you used during +A good rule of thumb is to take the maximum amount of memory you used during your session, and add 20-40% headroom for users to 'play around'. This is the maximum amount of memory that should be given to each user. -If users use *more* than this alloted amount of memory, their kernel will restart (and all -their progress in the current session will be lost). +If users use *more* than this alloted amount of memory, their notebook kernel will restart. CPU === diff --git a/docs/images/nbresuse.png b/docs/images/nbresuse.png new file mode 100644 index 0000000000000000000000000000000000000000..75a4644aea81ba8d657b08ac9bd6927623ea761a GIT binary patch literal 5609 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 literal 0 HcmV?d00001 diff --git a/docs/index.rst b/docs/index.rst index 9ff8929..eaf5931 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -61,6 +61,7 @@ How-To guides answer the question 'How do I...?' for a lot of topics. howto/share-data howto/notebook-interfaces howto/resource-estimation + howto/nbresuse Authentication --------------