Skip to content

Commit 683b470

Browse files
committed
Merge jupyterlab-kernel-usage
1 parent c32d676 commit 683b470

File tree

17 files changed

+2907
-2351
lines changed

17 files changed

+2907
-2351
lines changed

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

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: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import json
22
from concurrent.futures import ThreadPoolExecutor
33

4+
import ipykernel
45
import psutil
6+
import zmq
7+
from jupyter_client.jsonutil import date_default
58
from jupyter_server.base.handlers import APIHandler
9+
from packaging import version
610
from tornado import web
711
from tornado.concurrent import run_on_executor
812

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

1519

20+
USAGE_IS_SUPPORTED = version.parse("6.9.0") <= version.parse(ipykernel.__version__)
21+
22+
1623
class ApiHandler(APIHandler):
1724
executor = ThreadPoolExecutor(max_workers=5)
1825

@@ -74,3 +81,38 @@ def get_cpu_percent(p):
7481
return 0
7582

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

jupyter_resource_usage/server_extension.py

Lines changed: 10 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,15 @@ 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(base_url, "jupyterlab_kernel_usage", r"get_usage/(.+)$"),
27+
KernelUsageHandler,
28+
)
29+
],
30+
)
2131

2232
if resuseconfig.enable_prometheus_metrics:
2333
callback = ioloop.PeriodicCallback(

packages/labextension/package.json

Lines changed: 46 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,23 +42,58 @@
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/launcher": "^3.5.1",
49+
"@jupyterlab/notebook": "^3.5.1",
50+
"@jupyterlab/services": "^6.5.1",
51+
"@jupyterlab/statusbar": "^3.5.1",
52+
"@jupyterlab/translation": "^3.5.1",
53+
"@lumino/polling": "^1.11.3",
54+
"typestyle": "^2.4.0"
5255
},
5356
"devDependencies": {
54-
"@jupyterlab/builder": "^3.0.0",
57+
"@jupyterlab/builder": "^3.5.1",
58+
"@typescript-eslint/eslint-plugin": "^4.8.1",
59+
"@typescript-eslint/parser": "^4.8.1",
60+
"eslint": "^7.14.0",
61+
"eslint-config-prettier": "^6.15.0",
62+
"eslint-plugin-prettier": "^3.1.4",
63+
"mkdirp": "^1.0.3",
5564
"npm-run-all": "^4.1.5",
65+
"prettier": "^2.1.1",
5666
"rimraf": "^3.0.2",
57-
"typescript": "~4.0.3"
67+
"typescript": "~4.1.3"
68+
},
69+
"sideEffects": [
70+
"style/*.css",
71+
"style/index.js"
72+
],
73+
"styleModule": "style/index.js",
74+
"publishConfig": {
75+
"access": "public"
5876
},
5977
"jupyterlab": {
78+
"discovery": {
79+
"server": {
80+
"managers": [
81+
"pip"
82+
],
83+
"base": {
84+
"name": "jupyterlab_kernel_usage"
85+
}
86+
}
87+
},
6088
"extension": true,
6189
"outputDir": "../../jupyter_resource_usage/labextension"
90+
},
91+
"jupyter-releaser": {
92+
"hooks": {
93+
"before-build-npm": [
94+
"python -m pip install jupyterlab~=3.1",
95+
"jlpm"
96+
]
97+
}
6298
}
6399
}

packages/labextension/src/format.ts

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

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+
'jupyterlab_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+
}

packages/labextension/src/index.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,38 @@ import {
22
JupyterFrontEnd,
33
JupyterFrontEndPlugin,
44
} from '@jupyterlab/application';
5+
import { INotebookTracker } from '@jupyterlab/notebook';
6+
import { LabIcon } from '@jupyterlab/ui-components';
7+
import { ICommandPalette } from '@jupyterlab/apputils';
8+
import { ILauncher } from '@jupyterlab/launcher';
9+
import { KernelUsagePanel } from './panel';
10+
import tachometer from '../style/tachometer.svg';
511

612
import { IStatusBar } from '@jupyterlab/statusbar';
713

814
import { ITranslator } from '@jupyterlab/translation';
915

1016
import { MemoryUsage } from './memoryUsage';
1117

18+
namespace CommandIDs {
19+
export const getKernelUsage = 'kernel-usage:get';
20+
}
21+
1222
/**
1323
* Initialization data for the jupyter-resource-usage extension.
1424
*/
1525
const extension: JupyterFrontEndPlugin<void> = {
1626
id: '@jupyter-server/resource-usage:memory-status-item',
1727
autoStart: true,
18-
requires: [IStatusBar, ITranslator],
28+
requires: [IStatusBar, ITranslator, ICommandPalette, INotebookTracker],
29+
optional: [ILauncher],
1930
activate: (
2031
app: JupyterFrontEnd,
2132
statusBar: IStatusBar,
22-
translator: ITranslator
33+
translator: ITranslator,
34+
palette: ICommandPalette,
35+
notebookTracker: INotebookTracker,
36+
launcher: ILauncher | null
2337
) => {
2438
const item = new MemoryUsage(translator);
2539

@@ -30,6 +44,32 @@ const extension: JupyterFrontEndPlugin<void> = {
3044
isActive: () => item.model.metricsAvailable,
3145
activeStateChanged: item.model.stateChanged,
3246
});
47+
48+
const { commands, shell } = app;
49+
const category = 'Kernel Resource';
50+
51+
async function createPanel(): Promise<KernelUsagePanel> {
52+
const panel = new KernelUsagePanel({
53+
widgetAdded: notebookTracker.widgetAdded,
54+
currentNotebookChanged: notebookTracker.currentChanged,
55+
});
56+
shell.add(panel, 'right', { rank: 200 });
57+
return panel;
58+
}
59+
60+
commands.addCommand(CommandIDs.getKernelUsage, {
61+
label: 'Kernel Usage',
62+
caption: 'Kernel Usage',
63+
icon: new LabIcon({
64+
name: 'jupyterlab-kernel-usage:icon',
65+
svgstr: tachometer,
66+
}),
67+
execute: createPanel,
68+
});
69+
70+
palette.addItem({ command: CommandIDs.getKernelUsage, category });
71+
72+
createPanel();
3373
},
3474
};
3575

0 commit comments

Comments
 (0)