Skip to content

Commit 7fb8112

Browse files
authored
Merge pull request #45 from Gsbreddy/new-api-for-lab
Adding support for jupyterlab statusbar-extension #36
2 parents 434df3f + 8aa32c8 commit 7fb8112

File tree

7 files changed

+113
-54
lines changed

7 files changed

+113
-54
lines changed

CONTRIBUTING.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ We recommend using [pipenv](https://docs.pipenv.org/) to make development easier
1717
```
1818

1919
2. Create an environment that will hold our dependencies.
20-
20+
2121
```bash
2222
cd nbresuse
2323
pipenv --python 3.6
@@ -32,11 +32,9 @@ We recommend using [pipenv](https://docs.pipenv.org/) to make development easier
3232
4. Do a dev install of nbresuse and its dependencies
3333

3434
```bash
35-
pip install --editable .[resources]
35+
pip install --editable .[dev]
3636
```
3737

38-
To test the behavior of NBResuse without `psutil` installed, run `pip install --editable .` instead.
39-
4038
5. Install and enable the nbextension for use with Jupyter Classic Notebook.
4139

4240
```bash
@@ -73,7 +71,7 @@ the pre-commit hook should take care of how it should look. Here is how to set u
7371
```bash
7472
pre-commit run
7573
```
76-
74+
7775
which should run any autoformatting on your code
7876
and tell you about any errors it couldn't fix automatically.
7977
You may also install [black integration](https://github.com/ambv/black#editor-integration)

README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,9 @@ main toolbar in the notebook itself, refreshing every 5s.
2222
You can currently install this package from PyPI.
2323

2424
```bash
25-
pip install nbresuse[resources]
25+
pip install nbresuse
2626
```
2727

28-
The above command will install NBResuse along with `psutil` Python package (which is used for getting hardware usage information from the system). If you would like to install NBResuse _without_ `psutil` (in which case NBResuse does essentially nothing), run `pip install nbresuse` instead.
29-
3028
**If your notebook version is < 5.3**, you need to enable the extension manually.
3129

3230
```

nbresuse/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
from notebook.utils import url_path_join
12
from tornado import ioloop
23

4+
from nbresuse.api import ApiHandler
35
from nbresuse.config import ResourceUseDisplay
46
from nbresuse.metrics import PSUtilMetricsLoader
57
from nbresuse.prometheus import PrometheusHandler
@@ -32,6 +34,10 @@ def load_jupyter_server_extension(nbapp):
3234
"""
3335
resuseconfig = ResourceUseDisplay(parent=nbapp)
3436
nbapp.web_app.settings["nbresuse_display_config"] = resuseconfig
37+
base_url = nbapp.web_app.settings["base_url"]
38+
nbapp.web_app.add_handlers(
39+
".*", [(url_path_join(base_url, "/metrics"), ApiHandler)]
40+
)
3541
callback = ioloop.PeriodicCallback(
3642
PrometheusHandler(PSUtilMetricsLoader(nbapp)), 1000
3743
)

nbresuse/api.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import json
2+
from concurrent.futures import ThreadPoolExecutor
3+
4+
import psutil
5+
from notebook.base.handlers import IPythonHandler
6+
from tornado import web
7+
from tornado.concurrent import run_on_executor
8+
9+
try:
10+
# Traitlets >= 4.3.3
11+
from traitlets import Callable
12+
except ImportError:
13+
from .utils import Callable
14+
15+
16+
class ApiHandler(IPythonHandler):
17+
18+
executor = ThreadPoolExecutor(max_workers=5)
19+
20+
@web.authenticated
21+
async def get(self):
22+
"""
23+
Calculate and return current resource usage metrics
24+
"""
25+
config = self.settings["nbresuse_display_config"]
26+
27+
cur_process = psutil.Process()
28+
all_processes = [cur_process] + cur_process.children(recursive=True)
29+
30+
# Get memory information
31+
rss = sum([p.memory_info().rss for p in all_processes])
32+
33+
if callable(config.mem_limit):
34+
mem_limit = config.mem_limit(rss=rss)
35+
else: # mem_limit is an Int
36+
mem_limit = config.mem_limit
37+
38+
limits = {"memory": {"rss": mem_limit}}
39+
if config.mem_limit:
40+
limits["memory"]["warn"] = (mem_limit - rss) < (
41+
mem_limit * config.mem_warning_threshold
42+
)
43+
44+
metrics = {"rss": rss, "limits": limits}
45+
46+
# Optionally get CPU information
47+
if config.track_cpu_percent:
48+
cpu_count = psutil.cpu_count()
49+
cpu_percent = await self._get_cpu_percent(all_processes)
50+
51+
if config.cpu_limit != 0:
52+
limits["cpu"] = {"cpu": config.cpu_limit}
53+
if config.cpu_warning_threshold != 0:
54+
limits["cpu"]["warn"] = (config.cpu_limit - self.cpu_percent) < (
55+
config.cpu_limit * config.cpu_warning_threshold
56+
)
57+
58+
metrics.update(cpu_percent=cpu_percent, cpu_count=cpu_count)
59+
60+
self.write(json.dumps(metrics))
61+
62+
@run_on_executor
63+
def _get_cpu_percent(self, all_processes):
64+
def get_cpu_percent(p):
65+
try:
66+
return p.cpu_percent(interval=0.05)
67+
# Avoid littering logs with stack traces complaining
68+
# about dead processes having no CPU usage
69+
except:
70+
return 0
71+
72+
return sum([get_cpu_percent(p) for p in all_processes])

nbresuse/static/main.js

Lines changed: 27 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ define([
55
function setupDOM() {
66
$('#maintoolbar-container').append(
77
$('<div>').attr('id', 'nbresuse-display')
8-
.addClass('btn-group')
9-
.addClass('pull-right')
10-
.append(
11-
$('<strong>').text('Memory: ')
12-
).append(
8+
.addClass('btn-group')
9+
.addClass('pull-right')
10+
.append(
11+
$('<strong>').text('Memory: ')
12+
).append(
1313
$('<span>').attr('id', 'nbresuse-mem')
14-
.attr('title', 'Actively used Memory (updates every 5s)')
14+
.attr('title', 'Actively used Memory (updates every 5s)')
1515
)
1616
);
1717
// FIXME: Do something cleaner to get styles in here?
@@ -24,49 +24,35 @@ define([
2424
}
2525

2626
function humanFileSize(size) {
27-
var i = Math.floor( Math.log(size) / Math.log(1024) );
28-
return ( size / Math.pow(1024, i) ).toFixed(1) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i];
27+
var i = Math.floor(Math.log(size) / Math.log(1024));
28+
return (size / Math.pow(1024, i)).toFixed(1) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i];
2929
}
3030

31-
32-
function metric(metric_name, text, multiple=false) {
33-
var regex = new RegExp("^" + metric_name + "\{?([^ \}]*)\}? (.*)$", "gm");
34-
var matches = [];
35-
var match;
36-
37-
do{
38-
match = regex.exec(text);
39-
if (match){
40-
matches.push(match)
41-
}
42-
}
43-
while (match);
44-
45-
if (!multiple) {
46-
if (matches.length > 0)
47-
return matches[0];
48-
return null;
49-
}else
50-
return matches;
51-
}
52-
53-
var displayMetrics = function() {
31+
var displayMetrics = function () {
5432
if (document.hidden) {
5533
// Don't poll when nobody is looking
5634
return;
5735
}
58-
$.ajax({
36+
$.getJSON({
5937
url: utils.get_body_data('baseUrl') + 'metrics',
60-
success: function(data) {
61-
let totalMemoryUsage = metric("total_memory_usage", data);
62-
let maxMemoryUsage = metric("max_memory_usage", data);
38+
success: function (data) {
39+
totalMemoryUsage = humanFileSize(data['rss']);
40+
41+
var limits = data['limits'];
42+
var display = totalMemoryUsage;
6343

64-
if (maxMemoryUsage[2] <= 0)
65-
return;
66-
totalMemoryUsage = humanFileSize(parseFloat(totalMemoryUsage[2]));
67-
maxMemoryUsage = humanFileSize(parseFloat(maxMemoryUsage[2]));
44+
if (limits['memory']) {
45+
if (limits['memory']['rss']) {
46+
maxMemoryUsage = humanFileSize(limits['memory']['rss']);
47+
display += " / " + maxMemoryUsage
48+
}
49+
if (limits['memory']['warn']) {
50+
$('#nbresuse-display').addClass('nbresuse-warn');
51+
} else {
52+
$('#nbresuse-display').removeClass('nbresuse-warn');
53+
}
54+
}
6855

69-
var display = totalMemoryUsage + "/" + maxMemoryUsage;
7056
$('#nbresuse-mem').text(display);
7157
}
7258
});
@@ -78,7 +64,7 @@ define([
7864
// Update every five seconds, eh?
7965
setInterval(displayMetrics, 1000 * 5);
8066

81-
document.addEventListener("visibilitychange", function() {
67+
document.addEventListener("visibilitychange", function () {
8268
// Update instantly when user activates notebook tab
8369
// FIXME: Turn off update timer completely when tab not in focus
8470
if (!document.hidden) {

nbresuse/tests/test_basic.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def test_import_serverextension(self):
2525

2626
# mock a notebook app
2727
nbapp_mock = MagicMock()
28-
nbapp_mock.web_app.settings = {}
28+
nbapp_mock.web_app.settings = {"base_url": ""}
2929

3030
# mock these out for unit test
3131
with patch("tornado.ioloop.PeriodicCallback") as periodic_callback_mock, patch(

setup.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
setuptools.setup(
1313
name="nbresuse",
14-
version="0.4.0",
14+
version="0.3.4",
1515
url="https://github.com/yuvipanda/nbresuse",
1616
author="Yuvi Panda",
1717
description="Simple Jupyter extension to show how much resources (RAM) your notebook is using",
@@ -23,10 +23,9 @@
2323
"Programming Language :: Python :: 3",
2424
],
2525
packages=setuptools.find_packages(),
26-
install_requires=["notebook>=5.6.0", "prometheus_client"],
26+
install_requires=["notebook>=5.6.0", "prometheus_client", "psutil>=5.6.0"],
2727
extras_require={
28-
"resources": ["psutil>=5.6.0"],
29-
"dev": ["autopep8", "black", "pytest", "flake8", "pytest-cov>=2.6.1", "mock"],
28+
"dev": ["autopep8", "black", "pytest", "flake8", "pytest-cov>=2.6.1", "mock"]
3029
},
3130
data_files=[
3231
("share/jupyter/nbextensions/nbresuse", glob("nbresuse/static/*")),

0 commit comments

Comments
 (0)