Skip to content

Commit 28ad897

Browse files
authored
Merge pull request #164 from davidbrochart/jupyterlab-kernel-usage
Jupyterlab kernel usage
2 parents c32d676 + eb1c391 commit 28ad897

File tree

21 files changed

+2987
-2421
lines changed

21 files changed

+2987
-2421
lines changed

.github/workflows/build.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ jobs:
2525
- name: Install the extension
2626
run: |
2727
python -m pip install .
28+
jupyter server extension enable --py jupyter_resource_usage --sys-prefix
29+
jupyter serverextension enable --py jupyter_resource_usage --sys-prefix
30+
jupyter nbextension install --py jupyter_resource_usage --sys-prefix
31+
jupyter nbextension enable --py jupyter_resource_usage --sys-prefix
2832
2933
- name: Check the server, classic and lab extensions are installed
3034
run: |

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ python -m pip install -e ".[dev]"
9696
jupyter labextension develop . --overwrite
9797

9898
# go to the labextension directory
99-
cd labextension/
99+
cd packages/labextension/
100100

101101
# Rebuild extension Typescript source after making changes
102102
jlpm run build

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ displays an indication of how much resources your current notebook server and
1919
its children (kernels, terminals, etc) are using. This is displayed in the
2020
main toolbar in the notebook itself, refreshing every 5s.
2121

22+
Kernel resource usage can be displayed in a sidebar for IPython kernels with
23+
[ipykernel](https://github.com/ipython/ipykernel) >= 6.11.0.
24+
2225
## Installation
2326

2427
You can currently install this package from PyPI.
@@ -36,9 +39,9 @@ conda install -c conda-forge jupyter-resource-usage
3639
**If your notebook version is < 5.3**, you need to enable the extension manually.
3740

3841
```
39-
jupyter serverextension enable --py jupyter-resource-usage --sys-prefix
40-
jupyter nbextension install --py jupyter-resource-usage --sys-prefix
41-
jupyter nbextension enable --py jupyter-resource-usage --sys-prefix
42+
jupyter serverextension enable --py jupyter_resource_usage --sys-prefix
43+
jupyter nbextension install --py jupyter_resource_usage --sys-prefix
44+
jupyter nbextension enable --py jupyter_resource_usage --sys-prefix
4245
```
4346

4447
## Configuration

jupyter_resource_usage/__init__.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import json
2-
import os.path as osp
2+
from pathlib import Path
33

44
from ._version import __version__
55
from .server_extension import load_jupyter_server_extension
66

7-
HERE = osp.abspath(osp.dirname(__file__))
7+
HERE = Path(__file__).parent.resolve()
88

9-
with open(osp.join(HERE, "labextension", "package.json")) as fid:
10-
data = json.load(fid)
9+
data = json.loads((HERE / "labextension" / "package.json").read_text())
1110

1211

1312
def _jupyter_labextension_paths():

jupyter_resource_usage/api.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
from concurrent.futures import ThreadPoolExecutor
33

44
import psutil
5+
import zmq
6+
from jupyter_client.jsonutil import date_default
57
from jupyter_server.base.handlers import APIHandler
8+
from jupyter_server.utils import url_path_join
9+
from packaging import version
610
from tornado import web
711
from tornado.concurrent import run_on_executor
812

@@ -13,6 +17,16 @@
1317
from .utils import Callable
1418

1519

20+
try:
21+
import ipykernel
22+
23+
USAGE_IS_SUPPORTED = version.parse("6.9.0") <= version.parse(ipykernel.__version__)
24+
except ImportError:
25+
USAGE_IS_SUPPORTED = False
26+
27+
MAX_RETRIES = 3
28+
29+
1630
class ApiHandler(APIHandler):
1731
executor = ThreadPoolExecutor(max_workers=5)
1832

@@ -74,3 +88,37 @@ def get_cpu_percent(p):
7488
return 0
7589

7690
return sum([get_cpu_percent(p) for p in all_processes])
91+
92+
93+
class KernelUsageHandler(APIHandler):
94+
@web.authenticated
95+
async def get(self, matched_part=None, *args, **kwargs):
96+
97+
if not USAGE_IS_SUPPORTED:
98+
self.write(json.dumps({}))
99+
return
100+
101+
kernel_id = matched_part
102+
km = self.kernel_manager
103+
lkm = km.pinned_superclass.get_kernel(km, kernel_id)
104+
session = lkm.session
105+
client = lkm.client()
106+
107+
control_channel = client.control_channel
108+
usage_request = session.msg("usage_request", {})
109+
110+
control_channel.send(usage_request)
111+
poller = zmq.Poller()
112+
control_socket = control_channel.socket
113+
poller.register(control_socket, zmq.POLLIN)
114+
for i in range(1, MAX_RETRIES + 1):
115+
timeout_ms = 1000 * i
116+
events = dict(poller.poll(timeout_ms))
117+
if not events:
118+
self.write(json.dumps({}))
119+
break
120+
if control_socket not in events:
121+
continue
122+
res = await client.control_channel.get_msg(timeout=0)
123+
self.write(json.dumps(res, default=date_default))
124+
break

jupyter_resource_usage/server_extension.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from tornado import ioloop
33

44
from jupyter_resource_usage.api import ApiHandler
5+
from jupyter_resource_usage.api import KernelUsageHandler
56
from jupyter_resource_usage.config import ResourceUseDisplay
67
from jupyter_resource_usage.metrics import PSUtilMetricsLoader
78
from jupyter_resource_usage.prometheus import PrometheusHandler
@@ -18,6 +19,17 @@ def load_jupyter_server_extension(server_app):
1819
server_app.web_app.add_handlers(
1920
".*", [(url_path_join(base_url, "/api/metrics/v1"), ApiHandler)]
2021
)
22+
server_app.web_app.add_handlers(
23+
".*$",
24+
[
25+
(
26+
url_path_join(
27+
base_url, "/api/metrics/v1/kernel_usage", r"get_usage/(.+)$"
28+
),
29+
KernelUsageHandler,
30+
)
31+
],
32+
)
2133

2234
if resuseconfig.enable_prometheus_metrics:
2335
callback = ioloop.PeriodicCallback(

packages/labextension/package.json

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
],
2020
"main": "lib/index.js",
2121
"types": "lib/index.d.ts",
22+
"style": "style/index.css",
2223
"repository": {
2324
"type": "git",
2425
"url": "https://github.com/jupyter-server/jupyter-resource-usage.git"
@@ -41,22 +42,48 @@
4142
"watch:labextension": "jupyter labextension watch ."
4243
},
4344
"dependencies": {
44-
"@jupyterlab/application": "^3.0.0",
45-
"@jupyterlab/apputils": "^3.0.0",
46-
"@jupyterlab/coreutils": "^5.0.0",
47-
"@jupyterlab/services": "^6.0.0",
48-
"@jupyterlab/statusbar": "^3.0.0",
49-
"@jupyterlab/translation": "^3.0.0",
50-
"@lumino/polling": "^1.3.3",
51-
"typestyle": "^2.0.4"
45+
"@jupyterlab/application": "^3.5.1",
46+
"@jupyterlab/apputils": "^3.5.1",
47+
"@jupyterlab/coreutils": "^5.5.1",
48+
"@jupyterlab/notebook": "^3.5.1",
49+
"@jupyterlab/services": "^6.5.1",
50+
"@jupyterlab/statusbar": "^3.5.1",
51+
"@jupyterlab/translation": "^3.5.1",
52+
"@lumino/polling": "^1.11.3",
53+
"typestyle": "^2.4.0"
5254
},
5355
"devDependencies": {
54-
"@jupyterlab/builder": "^3.0.0",
56+
"@jupyterlab/builder": "^3.5.1",
57+
"@typescript-eslint/eslint-plugin": "^4.8.1",
58+
"@typescript-eslint/parser": "^4.8.1",
59+
"eslint": "^7.14.0",
60+
"eslint-config-prettier": "^6.15.0",
61+
"eslint-plugin-prettier": "^3.1.4",
62+
"mkdirp": "^1.0.3",
5563
"npm-run-all": "^4.1.5",
64+
"prettier": "^2.1.1",
5665
"rimraf": "^3.0.2",
57-
"typescript": "~4.0.3"
66+
"typescript": "~4.1.3"
67+
},
68+
"sideEffects": [
69+
"style/*.css",
70+
"style/index.js"
71+
],
72+
"styleModule": "style/index.js",
73+
"publishConfig": {
74+
"access": "public"
5875
},
5976
"jupyterlab": {
77+
"discovery": {
78+
"server": {
79+
"managers": [
80+
"pip"
81+
],
82+
"base": {
83+
"name": "jupyterlab_kernel_usage"
84+
}
85+
}
86+
},
6087
"extension": true,
6188
"outputDir": "../../jupyter_resource_usage/labextension"
6289
}

packages/labextension/src/format.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* The type of unit used for reporting memory usage.
3+
*/
4+
export type MemoryUnit = 'B' | 'KB' | 'MB' | 'GB' | 'TB' | 'PB';
5+
6+
/**
7+
* The number of bytes in each memory unit.
8+
*/
9+
export const MEMORY_UNIT_LIMITS: {
10+
readonly [U in MemoryUnit]: number;
11+
} = {
12+
B: 1,
13+
KB: 1024,
14+
MB: 1048576,
15+
GB: 1073741824,
16+
TB: 1099511627776,
17+
PB: 1125899906842624,
18+
};
19+
20+
export function formatForDisplay(numBytes: number | undefined): string {
21+
const lu = convertToLargestUnit(numBytes);
22+
return lu[0].toFixed(2) + ' ' + lu[1];
23+
}
24+
25+
/**
26+
* Given a number of bytes, convert to the most human-readable
27+
* format, (GB, TB, etc).
28+
*/
29+
export function convertToLargestUnit(
30+
numBytes: number | undefined
31+
): [number, MemoryUnit] {
32+
if (!numBytes) {
33+
return [0, 'B'];
34+
}
35+
if (numBytes < MEMORY_UNIT_LIMITS.KB) {
36+
return [numBytes, 'B'];
37+
} else if (
38+
MEMORY_UNIT_LIMITS.KB === numBytes ||
39+
numBytes < MEMORY_UNIT_LIMITS.MB
40+
) {
41+
return [numBytes / MEMORY_UNIT_LIMITS.KB, 'KB'];
42+
} else if (
43+
MEMORY_UNIT_LIMITS.MB === numBytes ||
44+
numBytes < MEMORY_UNIT_LIMITS.GB
45+
) {
46+
return [numBytes / MEMORY_UNIT_LIMITS.MB, 'MB'];
47+
} else if (
48+
MEMORY_UNIT_LIMITS.GB === numBytes ||
49+
numBytes < MEMORY_UNIT_LIMITS.TB
50+
) {
51+
return [numBytes / MEMORY_UNIT_LIMITS.GB, 'GB'];
52+
} else if (
53+
MEMORY_UNIT_LIMITS.TB === numBytes ||
54+
numBytes < MEMORY_UNIT_LIMITS.PB
55+
) {
56+
return [numBytes / MEMORY_UNIT_LIMITS.TB, 'TB'];
57+
} else {
58+
return [numBytes / MEMORY_UNIT_LIMITS.PB, 'PB'];
59+
}
60+
}

packages/labextension/src/handler.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { URLExt } from '@jupyterlab/coreutils';
2+
import { ServerConnection } from '@jupyterlab/services';
3+
4+
/**
5+
* Call the API extension
6+
*
7+
* @param endPoint API REST end point for the extension
8+
* @param init Initial values for the request
9+
* @returns The response body interpreted as JSON
10+
*/
11+
export async function requestAPI<T>(
12+
endPoint = '',
13+
init: RequestInit = {}
14+
): Promise<T> {
15+
const settings = ServerConnection.makeSettings();
16+
const requestUrl = URLExt.join(
17+
settings.baseUrl,
18+
'api/metrics/v1/kernel_usage', // API Namespace
19+
endPoint
20+
);
21+
22+
let response: Response;
23+
try {
24+
response = await ServerConnection.makeRequest(requestUrl, init, settings);
25+
} catch (error) {
26+
throw new ServerConnection.NetworkError(error as any);
27+
}
28+
29+
let data: any = await response.text();
30+
31+
if (data.length > 0) {
32+
try {
33+
data = JSON.parse(data);
34+
} catch (error) {
35+
console.log('Not a JSON response body.', response);
36+
}
37+
}
38+
39+
if (!response.ok) {
40+
throw new ServerConnection.ResponseError(response, data.message || data);
41+
}
42+
43+
return data;
44+
}

0 commit comments

Comments
 (0)