├── jupyter_resource_usage ├── tests │ ├── __init__.py │ └── test_basic.py ├── _version.py ├── utils.py ├── __init__.py ├── server_extension.py ├── prometheus.py ├── metrics.py ├── api.py ├── config.py └── static │ └── main.js ├── packages └── labextension │ ├── style │ ├── index.js │ ├── index.css │ ├── tachometer.svg │ └── base.css │ ├── src │ ├── svg.d.ts │ ├── text.ts │ ├── useInterval.ts │ ├── types.ts │ ├── handler.ts │ ├── panel.ts │ ├── cpuView.tsx │ ├── diskView.tsx │ ├── memoryView.tsx │ ├── format.ts │ ├── resourceUsage.tsx │ ├── tracker.ts │ ├── indicator.tsx │ ├── index.ts │ ├── model.ts │ └── widget.tsx │ ├── tsconfig.json │ ├── schema │ └── topbar-item.json │ └── package.json ├── .yarnrc.yml ├── binder ├── postBuild └── environment.yml ├── doc ├── topbar.gif ├── settings.png ├── statusbar.png ├── kernel-usage.png ├── statusbar-cpu.png ├── statusbar-warn.png ├── statusbar_disk.png └── kernel-usage-limited-info.png ├── setup.py ├── lerna.json ├── jupyter-config ├── nbconfig │ └── notebook.d │ │ └── jupyter_resource_usage.json ├── jupyter_server_config.d │ └── jupyter_resource_usage.json └── jupyter_notebook_config.d │ └── jupyter_resource_usage.json ├── .prettierignore ├── install.json ├── RELEASE.md ├── .github └── workflows │ ├── enforce-label.yml │ ├── lint.yml │ ├── check-release.yml │ ├── publish-changelog.yml │ ├── tests.yml │ ├── build.yml │ ├── prep-release.yml │ └── publish-release.yml ├── .flake8 ├── .pre-commit-config.yaml ├── .gitignore ├── .travis.yml ├── LICENSE ├── package.json ├── pyproject.toml ├── CONTRIBUTING.md ├── README.md └── CHANGELOG.md /jupyter_resource_usage/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /jupyter_resource_usage/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.2.0" 2 | -------------------------------------------------------------------------------- /packages/labextension/style/index.js: -------------------------------------------------------------------------------- 1 | import './base.css'; 2 | -------------------------------------------------------------------------------- /packages/labextension/style/index.css: -------------------------------------------------------------------------------- 1 | @import url('base.css'); 2 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | enableImmutableInstalls: false 2 | nodeLinker: node-modules 3 | -------------------------------------------------------------------------------- /binder/postBuild: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | python -m pip install . --upgrade 5 | -------------------------------------------------------------------------------- /doc/topbar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter-server/jupyter-resource-usage/HEAD/doc/topbar.gif -------------------------------------------------------------------------------- /doc/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter-server/jupyter-resource-usage/HEAD/doc/settings.png -------------------------------------------------------------------------------- /doc/statusbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter-server/jupyter-resource-usage/HEAD/doc/statusbar.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # setup.py shim for use with applications that require it. 2 | __import__("setuptools").setup() 3 | -------------------------------------------------------------------------------- /doc/kernel-usage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter-server/jupyter-resource-usage/HEAD/doc/kernel-usage.png -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmClient": "jlpm", 3 | "version": "independent", 4 | "useWorkspaces": true 5 | } 6 | -------------------------------------------------------------------------------- /doc/statusbar-cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter-server/jupyter-resource-usage/HEAD/doc/statusbar-cpu.png -------------------------------------------------------------------------------- /doc/statusbar-warn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter-server/jupyter-resource-usage/HEAD/doc/statusbar-warn.png -------------------------------------------------------------------------------- /doc/statusbar_disk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter-server/jupyter-resource-usage/HEAD/doc/statusbar_disk.png -------------------------------------------------------------------------------- /packages/labextension/src/svg.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const value: string; 3 | export default value; 4 | } 5 | -------------------------------------------------------------------------------- /doc/kernel-usage-limited-info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyter-server/jupyter-resource-usage/HEAD/doc/kernel-usage-limited-info.png -------------------------------------------------------------------------------- /jupyter-config/nbconfig/notebook.d/jupyter_resource_usage.json: -------------------------------------------------------------------------------- 1 | { 2 | "load_extensions": { 3 | "jupyter_resource_usage/main": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/node_modules 3 | **/lib 4 | **/package.json 5 | 6 | jupyter_resource_usage/labextension 7 | jupyter_resource_usage/static 8 | -------------------------------------------------------------------------------- /binder/environment.yml: -------------------------------------------------------------------------------- 1 | name: jupyter-resource-usage 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - python=3.10 6 | - jupyterlab=4 7 | - notebook=7 8 | - nodejs=18 9 | -------------------------------------------------------------------------------- /jupyter-config/jupyter_server_config.d/jupyter_resource_usage.json: -------------------------------------------------------------------------------- 1 | { 2 | "ServerApp": { 3 | "jpserver_extensions": { 4 | "jupyter_resource_usage": true 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /jupyter-config/jupyter_notebook_config.d/jupyter_resource_usage.json: -------------------------------------------------------------------------------- 1 | { 2 | "NotebookApp": { 3 | "nbserver_extensions": { 4 | "jupyter_resource_usage": true 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /install.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "python", 3 | "packageName": "jupyter-resource-usage", 4 | "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package jupyter-resource-usage" 5 | } 6 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Making a new release of `jupyter-resource-usage` 2 | 3 | ## Automated Releases with `jupyter_releaser` 4 | 5 | The recommended way to make a release is to use 6 | [`jupyter_releaser`](https://jupyter-releaser.readthedocs.io/en/latest/). 7 | -------------------------------------------------------------------------------- /.github/workflows/enforce-label.yml: -------------------------------------------------------------------------------- 1 | name: Enforce PR label 2 | 3 | on: 4 | pull_request: 5 | types: [labeled, unlabeled, opened, edited, synchronize] 6 | jobs: 7 | enforce-label: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | pull-requests: write 11 | steps: 12 | - name: enforce-triage-label 13 | uses: jupyterlab/maintainer-tools/.github/actions/enforce-label@v1 14 | -------------------------------------------------------------------------------- /packages/labextension/src/text.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jupyter Development Team. 2 | // Distributed under the terms of the Modified BSD License. 3 | 4 | import { style } from 'typestyle'; 5 | 6 | export const resourceItem = style( 7 | { 8 | fontSize: 'var(--jp-ui-font-size1)', 9 | fontFamily: 'var(--jp-ui-font-family)', 10 | }, 11 | { 12 | backgroundColor: '#FFD2D2', 13 | color: '#D8000C', 14 | } 15 | ); 16 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length=200 3 | # Ignore style and complexity 4 | # E: style errors 5 | # W: style warnings 6 | # F401: module imported but unused 7 | # F811: redefinition of unused `name` from line `N` 8 | # F841: local variable assigned but never used 9 | ignore = E, C, W, F401, F403, F811, F841, E402, I100, I101, D400 10 | exclude = 11 | jupyter-resource-usage/tests, 12 | helm-chart, 13 | hooks, 14 | setup.py, 15 | statuspage, 16 | versioneer.py 17 | -------------------------------------------------------------------------------- /jupyter_resource_usage/utils.py: -------------------------------------------------------------------------------- 1 | import six 2 | from traitlets import TraitType 3 | 4 | # copy-pasted from the master of Traitlets source 5 | 6 | 7 | class Callable(TraitType): 8 | """A trait which is callable. 9 | Notes 10 | ----- 11 | Classes are callable, as are instances 12 | with a __call__() method.""" 13 | 14 | info_text = "a callable" 15 | 16 | def validate(self, obj, value): 17 | if six.callable(value): 18 | return value 19 | else: 20 | self.error(obj, value) 21 | -------------------------------------------------------------------------------- /packages/labextension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "composite": true, 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "incremental": true, 8 | "jsx": "react", 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "noEmitOnError": true, 12 | "noImplicitAny": true, 13 | "noUnusedLocals": true, 14 | "preserveWatchOutput": true, 15 | "resolveJsonModule": true, 16 | "outDir": "lib", 17 | "rootDir": "src", 18 | "strict": true, 19 | "strictNullChecks": true, 20 | "target": "es2018", 21 | "types": [] 22 | }, 23 | "include": ["src/**/*"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/labextension/src/useInterval.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | const useInterval = (callback: () => any, delay: number): void => { 4 | const savedCallback = useRef<() => any>(); 5 | 6 | useEffect(() => { 7 | savedCallback.current = callback; 8 | }, [callback]); 9 | 10 | useEffect(() => { 11 | function tick() { 12 | if (savedCallback.current) { 13 | savedCallback.current(); 14 | } 15 | } 16 | if (delay !== null) { 17 | const id = setInterval(tick, delay); 18 | return () => { 19 | clearInterval(id); 20 | }; 21 | } 22 | }, [callback, delay]); 23 | }; 24 | 25 | export default useInterval; 26 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/asottile/reorder-python-imports 3 | rev: v3.12.0 4 | hooks: 5 | - id: reorder-python-imports 6 | language_version: python3 7 | - repo: https://github.com/psf/black 8 | rev: 24.2.0 9 | hooks: 10 | - id: black 11 | - repo: https://github.com/PyCQA/flake8 12 | rev: "7.0.0" 13 | hooks: 14 | - id: flake8 15 | - repo: https://github.com/pre-commit/pre-commit-hooks 16 | rev: v4.5.0 17 | hooks: 18 | - id: end-of-file-fixer 19 | - id: check-json 20 | - id: check-yaml 21 | exclude: ^helm-chart/nbviewer/templates/ 22 | - id: check-case-conflict 23 | - id: check-executables-have-shebangs 24 | - id: requirements-txt-fixer 25 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | 18 | - name: Base Setup 19 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 20 | 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip black jupyterlab~=4.0 24 | 25 | - name: Lint Python 26 | run: | 27 | black --check . 28 | 29 | - name: Lint JS 30 | run: | 31 | jlpm 32 | jlpm run lint:check 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | ### Python ### 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # Distribution / packaging 10 | .Python 11 | .direnv 12 | .envrc 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | *.ipynb 29 | MANIFEST 30 | 31 | # IDE and code editors 32 | *.iml 33 | .idea 34 | .vscode 35 | *.code-workspace 36 | .history 37 | 38 | # Labextension 39 | *.bundle.* 40 | lib/ 41 | node_modules/ 42 | *.egg-info/ 43 | .ipynb_checkpoints 44 | *.tsbuildinfo 45 | jupyter_resource_usage/labextension 46 | yarn-error.log 47 | .yarn/ 48 | .pnp* 49 | -------------------------------------------------------------------------------- /.github/workflows/check-release.yml: -------------------------------------------------------------------------------- 1 | name: Check Release 2 | on: 3 | push: 4 | branches: 5 | - '*' 6 | pull_request: 7 | branches: 8 | - '*' 9 | 10 | permissions: 11 | contents: write 12 | 13 | jobs: 14 | check_release: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 19 | - name: Check Release 20 | uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v2 21 | with: 22 | token: ${{ secrets.GITHUB_TOKEN }} 23 | - name: Upload Distributions 24 | uses: actions/upload-artifact@v4 25 | with: 26 | name: jupyter-resource-usage-releaser-dist-${{ github.run_number }} 27 | path: .jupyter_releaser_checkout/dist 28 | -------------------------------------------------------------------------------- /packages/labextension/src/types.ts: -------------------------------------------------------------------------------- 1 | import { Widget } from '@lumino/widgets'; 2 | import { ISessionContext } from '@jupyterlab/apputils'; 3 | import { NotebookPanel } from '@jupyterlab/notebook'; 4 | import { ConsolePanel } from '@jupyterlab/console'; 5 | 6 | /** 7 | * Interface used to abstract away widgets with kernel to avoid introducing 8 | * spurious dependencies on properties specific to e.g. notebook or console. 9 | */ 10 | export interface IWidgetWithSession extends Widget { 11 | /** 12 | * Session context providing kernel access. 13 | */ 14 | sessionContext: ISessionContext; 15 | } 16 | 17 | /** 18 | * Check if given widget is one of the widgets known to have kernel session. 19 | * 20 | * Note: we could switch to duck-typing in future. 21 | */ 22 | export function hasKernelSession( 23 | widget: Widget 24 | ): widget is ConsolePanel | NotebookPanel { 25 | return widget instanceof ConsolePanel || widget instanceof NotebookPanel; 26 | } 27 | -------------------------------------------------------------------------------- /jupyter_resource_usage/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from ._version import __version__ 4 | from .server_extension import load_jupyter_server_extension 5 | 6 | 7 | def _jupyter_labextension_paths(): 8 | return [{"src": "labextension", "dest": "@jupyter-server/resource-usage"}] 9 | 10 | 11 | def _jupyter_server_extension_points(): 12 | """ 13 | Set up the server extension for collecting metrics 14 | """ 15 | return [{"module": "jupyter_resource_usage"}] 16 | 17 | 18 | def _jupyter_nbextension_paths(): 19 | """ 20 | Set up the notebook extension for displaying metrics 21 | """ 22 | return [ 23 | { 24 | "section": "notebook", 25 | "dest": "jupyter_resource_usage", 26 | "src": "static", 27 | "require": "jupyter_resource_usage/main", 28 | } 29 | ] 30 | 31 | 32 | # For backward compatibility 33 | _load_jupyter_server_extension = load_jupyter_server_extension 34 | _jupyter_server_extension_paths = _jupyter_server_extension_points 35 | -------------------------------------------------------------------------------- /packages/labextension/style/tachometer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.github/workflows/publish-changelog.yml: -------------------------------------------------------------------------------- 1 | name: "Publish Changelog" 2 | on: 3 | release: 4 | types: [published] 5 | 6 | workflow_dispatch: 7 | inputs: 8 | branch: 9 | description: "The branch to target" 10 | required: false 11 | 12 | jobs: 13 | publish_changelog: 14 | runs-on: ubuntu-latest 15 | environment: release 16 | steps: 17 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 18 | 19 | - uses: actions/create-github-app-token@v1 20 | id: app-token 21 | with: 22 | app-id: ${{ vars.APP_ID }} 23 | private-key: ${{ secrets.APP_PRIVATE_KEY }} 24 | 25 | - name: Publish changelog 26 | id: publish-changelog 27 | uses: jupyter-server/jupyter_releaser/.github/actions/publish-changelog@v2 28 | with: 29 | token: ${{ steps.app-token.outputs.token }} 30 | branch: ${{ github.event.inputs.branch }} 31 | 32 | - name: "** Next Step **" 33 | run: | 34 | echo "Merge the changelog update PR: ${{ steps.publish-changelog.outputs.pr_url }}" 35 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - 3.7 5 | - 3.8 6 | 7 | before_install: 8 | - pip install --upgrade setuptools pip 9 | 10 | install: 11 | - pip install --editable . 12 | 13 | script: 14 | - python -m pytest -vvv jupyter-resource-usage 15 | 16 | 17 | jobs: 18 | include: 19 | - name: autoformatting check 20 | python: 3.6 21 | # NOTE: It does not suffice to override to: null, [], or [""]. Travis will 22 | # fall back to the default if we do. 23 | before_install: echo "Do nothing before install." 24 | install: pip install pre-commit 25 | script: 26 | - pre-commit run --all-files 27 | after_success: echo "Do nothing after success." 28 | after_failure: 29 | - | 30 | echo "You can install pre-commit hooks to automatically run formatting" 31 | echo "on each commit with:" 32 | echo " pre-commit install" 33 | echo "or you can run by hand on staged files with" 34 | echo " pre-commit run" 35 | echo "or after-the-fact on already committed files with" 36 | echo " pre-commit run --all-files" 37 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | os: [ubuntu-latest, macos-latest, windows-latest] 16 | python-version: ["3.9", "3.13"] 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | 22 | - name: Base Setup 23 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 24 | 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install -e ".[dev]" 28 | 29 | - name: Lint with flake8 30 | run: | 31 | python -m flake8 jupyter_resource_usage 32 | 33 | - name: Test with pytest 34 | run: | 35 | python -m pytest -vvv jupyter_resource_usage --cov=jupyter_resource_usage --junitxml=python_junit.xml --cov-report=xml --cov-branch 36 | 37 | check_links: 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@v4 41 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 42 | - uses: jupyterlab/maintainer-tools/.github/actions/check-links@v1 43 | -------------------------------------------------------------------------------- /packages/labextension/src/handler.ts: -------------------------------------------------------------------------------- 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( 12 | endPoint = '', 13 | init: RequestInit = {} 14 | ): Promise { 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 | } 45 | -------------------------------------------------------------------------------- /packages/labextension/src/panel.ts: -------------------------------------------------------------------------------- 1 | import { Message } from '@lumino/messaging'; 2 | import { TranslationBundle } from '@jupyterlab/translation'; 3 | import { StackedPanel } from '@lumino/widgets'; 4 | import { LabIcon } from '@jupyterlab/ui-components'; 5 | import { KernelUsageWidget } from './widget'; 6 | import { KernelWidgetTracker } from './tracker'; 7 | 8 | import tachometer from '../style/tachometer.svg'; 9 | 10 | const PANEL_CLASS = 'jp-KernelUsage-view'; 11 | 12 | export class KernelUsagePanel extends StackedPanel { 13 | constructor(props: { 14 | tracker: KernelWidgetTracker; 15 | trans: TranslationBundle; 16 | }) { 17 | super(); 18 | this.addClass(PANEL_CLASS); 19 | this.id = 'kernelusage-panel-id'; 20 | this.title.caption = props.trans.__('Kernel Usage'); 21 | this.title.icon = new LabIcon({ 22 | name: 'jupyterlab-kernel-usage:icon', 23 | svgstr: tachometer, 24 | }); 25 | 26 | const widget = new KernelUsageWidget({ 27 | tracker: props.tracker, 28 | panel: this, 29 | trans: props.trans, 30 | }); 31 | this.addWidget(widget); 32 | } 33 | 34 | dispose(): void { 35 | super.dispose(); 36 | } 37 | 38 | protected onCloseRequest(msg: Message): void { 39 | super.onCloseRequest(msg); 40 | this.dispose(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Yuvi Panda 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /packages/labextension/style/base.css: -------------------------------------------------------------------------------- 1 | .jp-KernelUsage-view { 2 | min-width: var(--jp-sidebar-min-width); 3 | color: var(--jp-ui-font-color1); 4 | background: var(--jp-layout-color1); 5 | display: flex; 6 | flex-direction: column; 7 | font-size: var(--jp-ui-font-size1); 8 | } 9 | 10 | .jp-KernelUsage-section-separator { 11 | margin-top: var(--jp-content-heading-margin-top); 12 | margin-bottom: var(--jp-content-heading-margin-bottom); 13 | } 14 | 15 | .jp-KernelUsage-separator { 16 | margin-top: 10px; 17 | margin-bottom: 10px; 18 | } 19 | 20 | .jp-KernelUsage-timedOut { 21 | color: var(--jp-ui-font-color3); 22 | } 23 | 24 | .jp-KernelUsage-content { 25 | padding: 10px; 26 | overflow-y: auto; 27 | } 28 | 29 | .jp-IndicatorContainer { 30 | display: flex; 31 | flex-direction: row; 32 | margin-left: 1em; 33 | } 34 | 35 | .jp-IndicatorFiller { 36 | height: 100%; 37 | } 38 | 39 | .jp-IndicatorText { 40 | display: flex; 41 | min-width: 35px; 42 | flex-direction: column; 43 | justify-content: center; 44 | text-align: right; 45 | white-space: nowrap; 46 | overflow: hidden; 47 | } 48 | 49 | .jp-IndicatorWrapper { 50 | display: flex; 51 | flex-direction: column; 52 | justify-content: center; 53 | margin-left: 5px; 54 | margin-right: 5px; 55 | width: 75px; 56 | } 57 | 58 | .jp-IndicatorBar { 59 | height: 75%; 60 | outline: 1px solid black; 61 | } 62 | 63 | .jp-IndicatorBar svg { 64 | max-width: 100%; 65 | height: 100%; 66 | } 67 | 68 | .jp-TopBar-item .jp-IndicatorContainer { 69 | max-width: 500px; 70 | } 71 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: '*' 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - name: Base Setup 20 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 21 | 22 | - name: Install dependencies 23 | run: python -m pip install jupyterlab~=4.0 nbclassic~=1.0 24 | 25 | - name: Install the extension 26 | run: | 27 | python -m pip install . 28 | jupyter server extension enable --py jupyter_resource_usage --sys-prefix 29 | jupyter nbclassic-serverextension enable --py jupyter_resource_usage --sys-prefix 30 | jupyter nbclassic-extension install --py jupyter_resource_usage --sys-prefix 31 | jupyter nbclassic-extension enable --py jupyter_resource_usage --sys-prefix 32 | 33 | - name: Check the server, classic and lab extensions are installed 34 | run: | 35 | jupyter server extension list 2>&1 | grep -ie "jupyter_resource_usage.*enabled" 36 | jupyter nbclassic-serverextension list 2>&1 | grep -ie "jupyter_resource_usage.*enabled" 37 | jupyter nbclassic-extension list 2>&1 | grep -ie "jupyter_resource_usage/main.*enabled" 38 | jupyter labextension list 39 | jupyter labextension list 2>&1 | grep -ie "@jupyter-server/resource-usage.*OK" 40 | python -m jupyterlab.browser_check 41 | -------------------------------------------------------------------------------- /jupyter_resource_usage/server_extension.py: -------------------------------------------------------------------------------- 1 | from jupyter_server.utils import url_path_join 2 | from tornado import ioloop 3 | 4 | from jupyter_resource_usage.api import ApiHandler 5 | from jupyter_resource_usage.api import KernelUsageHandler 6 | from jupyter_resource_usage.config import ResourceUseDisplay 7 | from jupyter_resource_usage.metrics import PSUtilMetricsLoader 8 | from jupyter_resource_usage.prometheus import PrometheusHandler 9 | 10 | 11 | def load_jupyter_server_extension(server_app): 12 | """ 13 | Called during notebook start 14 | """ 15 | resuseconfig = ResourceUseDisplay(parent=server_app) 16 | server_app.web_app.settings["jupyter_resource_usage_display_config"] = resuseconfig 17 | base_url = server_app.web_app.settings["base_url"] 18 | 19 | server_app.web_app.add_handlers( 20 | ".*", [(url_path_join(base_url, "/api/metrics/v1"), ApiHandler)] 21 | ) 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 | ) 33 | 34 | if resuseconfig.enable_prometheus_metrics: 35 | callback = ioloop.PeriodicCallback( 36 | PrometheusHandler(PSUtilMetricsLoader(server_app)), 1000 37 | ) 38 | callback.start() 39 | else: 40 | server_app.log.info( 41 | "Prometheus metrics reporting disabled in jupyter_resource_usage." 42 | ) 43 | -------------------------------------------------------------------------------- /packages/labextension/src/cpuView.tsx: -------------------------------------------------------------------------------- 1 | import { ReactWidget } from '@jupyterlab/apputils'; 2 | 3 | import React, { useEffect, useState, ReactElement } from 'react'; 4 | 5 | import { IndicatorComponent } from './indicator'; 6 | 7 | import { ResourceUsage } from './model'; 8 | 9 | export const DEFAULT_CPU_LABEL = 'CPU: '; 10 | 11 | /** 12 | * A CpuView component to display CPU usage. 13 | */ 14 | const CpuViewComponent = ({ 15 | model, 16 | label, 17 | }: { 18 | model: ResourceUsage.Model; 19 | label: string; 20 | }): ReactElement => { 21 | const [text, setText] = useState(''); 22 | const [values, setValues] = useState([]); 23 | 24 | const update = (): void => { 25 | const { cpuLimit, currentCpuPercent } = model; 26 | const newValues = model.values.map((value) => 27 | Math.min(1, value.cpuPercent / (cpuLimit || 1)) 28 | ); 29 | const newText = `${(currentCpuPercent * 100).toFixed(0)}%`; 30 | setText(newText); 31 | setValues(newValues); 32 | }; 33 | 34 | useEffect(() => { 35 | model.stateChanged.connect(update); 36 | return (): void => { 37 | model.stateChanged.disconnect(update); 38 | }; 39 | }, [model]); 40 | 41 | return ( 42 | 49 | ); 50 | }; 51 | 52 | /** 53 | * A namespace for CpuView statics. 54 | */ 55 | export namespace CpuView { 56 | /** 57 | * Create a new CpuView React Widget. 58 | * 59 | * @param model The resource usage model. 60 | * @param label The label next to the component. 61 | */ 62 | export const createCpuView = ( 63 | model: ResourceUsage.Model, 64 | label: string 65 | ): ReactWidget => { 66 | return ReactWidget.create(); 67 | }; 68 | } 69 | -------------------------------------------------------------------------------- /.github/workflows/prep-release.yml: -------------------------------------------------------------------------------- 1 | name: "Step 1: Prep Release" 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version_spec: 6 | description: "New Version Specifier" 7 | default: "next" 8 | required: false 9 | branch: 10 | description: "The branch to target" 11 | required: false 12 | post_version_spec: 13 | description: "Post Version Specifier" 14 | required: false 15 | silent: 16 | description: "Set a placeholder in the changelog and don't publish the release." 17 | required: false 18 | type: boolean 19 | since: 20 | description: "Use PRs with activity since this date or git reference" 21 | required: false 22 | since_last_stable: 23 | description: "Use PRs with activity since the last stable git tag" 24 | required: false 25 | type: boolean 26 | jobs: 27 | prep_release: 28 | runs-on: ubuntu-latest 29 | permissions: 30 | contents: write 31 | steps: 32 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 33 | 34 | - name: Prep Release 35 | id: prep-release 36 | uses: jupyter-server/jupyter_releaser/.github/actions/prep-release@v2 37 | with: 38 | token: ${{ secrets.GITHUB_TOKEN }} 39 | version_spec: ${{ github.event.inputs.version_spec }} 40 | silent: ${{ github.event.inputs.silent }} 41 | post_version_spec: ${{ github.event.inputs.post_version_spec }} 42 | target: ${{ github.event.inputs.target }} 43 | branch: ${{ github.event.inputs.branch }} 44 | since: ${{ github.event.inputs.since }} 45 | since_last_stable: ${{ github.event.inputs.since_last_stable }} 46 | 47 | - name: "** Next Step **" 48 | run: | 49 | echo "Optional): Review Draft Release: ${{ steps.prep-release.outputs.release_url }}" 50 | -------------------------------------------------------------------------------- /packages/labextension/src/diskView.tsx: -------------------------------------------------------------------------------- 1 | import { ReactWidget } from '@jupyterlab/apputils'; 2 | 3 | import React, { useState, useEffect, ReactElement } from 'react'; 4 | 5 | import { IndicatorComponent } from './indicator'; 6 | 7 | import { ResourceUsage } from './model'; 8 | 9 | export const DEFAULT_DISK_LABEL = 'Disk: '; 10 | 11 | /** 12 | * A DiskView component to display disk usage. 13 | */ 14 | const DiskViewComponent = ({ 15 | model, 16 | label, 17 | }: { 18 | model: ResourceUsage.Model; 19 | label: string; 20 | }): ReactElement => { 21 | const [text, setText] = useState(''); 22 | const [values, setValues] = useState([]); 23 | 24 | const update = (): void => { 25 | const { maxDisk, currentDisk, diskUnits } = model; 26 | const precision = ['B', 'KB', 'MB'].indexOf(diskUnits) > 0 ? 0 : 2; 27 | const newText = `${currentDisk.toFixed(precision)} / ${maxDisk.toFixed( 28 | precision 29 | )} ${diskUnits}`; 30 | const newValues = model.values.map((value) => value.diskPercent); 31 | setText(newText); 32 | setValues(newValues); 33 | }; 34 | 35 | useEffect(() => { 36 | model.stateChanged.connect(update); 37 | return (): void => { 38 | model.stateChanged.disconnect(update); 39 | }; 40 | }, [model]); 41 | return ( 42 | 49 | ); 50 | }; 51 | 52 | export namespace DiskView { 53 | /** 54 | * Create a new MemoryView React Widget. 55 | * 56 | * @param model The resource usage model. 57 | * @param label The label next to the component. 58 | */ 59 | export const createDiskView = ( 60 | model: ResourceUsage.Model, 61 | label: string 62 | ): ReactWidget => { 63 | return ReactWidget.create( 64 | 65 | ); 66 | }; 67 | } 68 | -------------------------------------------------------------------------------- /packages/labextension/src/memoryView.tsx: -------------------------------------------------------------------------------- 1 | import { ReactWidget } from '@jupyterlab/apputils'; 2 | 3 | import React, { useState, useEffect, ReactElement } from 'react'; 4 | 5 | import { IndicatorComponent } from './indicator'; 6 | 7 | import { ResourceUsage } from './model'; 8 | 9 | export const DEFAULT_MEMORY_LABEL = 'Mem: '; 10 | 11 | /** 12 | * A MemoryView component to display memory usage. 13 | */ 14 | const MemoryViewComponent = ({ 15 | model, 16 | label, 17 | }: { 18 | model: ResourceUsage.Model; 19 | label: string; 20 | }): ReactElement => { 21 | const [text, setText] = useState(''); 22 | const [values, setValues] = useState([]); 23 | 24 | const update = (): void => { 25 | const { memoryLimit, currentMemory, memUnits } = model; 26 | const precision = ['B', 'KB', 'MB', 'GB'].indexOf(memUnits) > 0 ? 0 : 3; 27 | const newText = `${currentMemory.toFixed(precision)} ${ 28 | memoryLimit ? '/ ' + memoryLimit.toFixed(precision) : '' 29 | } ${memUnits}`; 30 | const newValues = model.values.map((value) => value.memoryPercent); 31 | setText(newText); 32 | setValues(newValues); 33 | }; 34 | 35 | useEffect(() => { 36 | model.stateChanged.connect(update); 37 | return (): void => { 38 | model.stateChanged.disconnect(update); 39 | }; 40 | }, [model]); 41 | 42 | return ( 43 | 50 | ); 51 | }; 52 | 53 | export namespace MemoryView { 54 | /** 55 | * Create a new MemoryView React Widget. 56 | * 57 | * @param model The resource usage model. 58 | * @param label The label next to the component. 59 | */ 60 | export const createMemoryView = ( 61 | model: ResourceUsage.Model, 62 | label: string 63 | ): ReactWidget => { 64 | return ReactWidget.create( 65 | 66 | ); 67 | }; 68 | } 69 | -------------------------------------------------------------------------------- /packages/labextension/src/format.ts: -------------------------------------------------------------------------------- 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( 21 | numBytes: number | undefined, 22 | units?: MemoryUnit | undefined 23 | ): string { 24 | const lu = convertToLargestUnit(numBytes, units); 25 | return lu[0].toFixed(2) + ' ' + lu[1]; 26 | } 27 | 28 | /** 29 | * Given a number of bytes, convert to the most human-readable 30 | * format, (GB, TB, etc). 31 | * If "units" is given, convert to that scale 32 | */ 33 | export function convertToLargestUnit( 34 | numBytes: number | undefined, 35 | units?: MemoryUnit 36 | ): [number, MemoryUnit] { 37 | if (!numBytes) { 38 | return [0, 'B']; 39 | } 40 | if (units && units in MEMORY_UNIT_LIMITS) { 41 | return [numBytes / MEMORY_UNIT_LIMITS[units], units]; 42 | } else if (numBytes < MEMORY_UNIT_LIMITS.KB) { 43 | return [numBytes, 'B']; 44 | } else if ( 45 | MEMORY_UNIT_LIMITS.KB === numBytes || 46 | numBytes < MEMORY_UNIT_LIMITS.MB 47 | ) { 48 | return [numBytes / MEMORY_UNIT_LIMITS.KB, 'KB']; 49 | } else if ( 50 | MEMORY_UNIT_LIMITS.MB === numBytes || 51 | numBytes < MEMORY_UNIT_LIMITS.GB 52 | ) { 53 | return [numBytes / MEMORY_UNIT_LIMITS.MB, 'MB']; 54 | } else if ( 55 | MEMORY_UNIT_LIMITS.GB === numBytes || 56 | numBytes < MEMORY_UNIT_LIMITS.TB 57 | ) { 58 | return [numBytes / MEMORY_UNIT_LIMITS.GB, 'GB']; 59 | } else if ( 60 | MEMORY_UNIT_LIMITS.TB === numBytes || 61 | numBytes < MEMORY_UNIT_LIMITS.PB 62 | ) { 63 | return [numBytes / MEMORY_UNIT_LIMITS.TB, 'TB']; 64 | } else { 65 | return [numBytes / MEMORY_UNIT_LIMITS.PB, 'PB']; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: "Step 2: Publish Release" 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | branch: 6 | description: "The target branch" 7 | required: false 8 | release_url: 9 | description: "The URL of the draft GitHub release" 10 | required: false 11 | steps_to_skip: 12 | description: "Comma separated list of steps to skip" 13 | required: false 14 | 15 | jobs: 16 | publish_release: 17 | runs-on: ubuntu-latest 18 | environment: release 19 | permissions: 20 | id-token: write 21 | steps: 22 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 23 | 24 | - uses: actions/create-github-app-token@v1 25 | id: app-token 26 | with: 27 | app-id: ${{ vars.APP_ID }} 28 | private-key: ${{ secrets.APP_PRIVATE_KEY }} 29 | 30 | - name: Populate Release 31 | id: populate-release 32 | uses: jupyter-server/jupyter_releaser/.github/actions/populate-release@v2 33 | with: 34 | token: ${{ steps.app-token.outputs.token }} 35 | branch: ${{ github.event.inputs.branch }} 36 | release_url: ${{ github.event.inputs.release_url }} 37 | steps_to_skip: ${{ github.event.inputs.steps_to_skip }} 38 | 39 | - name: Finalize Release 40 | id: finalize-release 41 | env: 42 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 43 | uses: jupyter-server/jupyter_releaser/.github/actions/finalize-release@v2 44 | with: 45 | token: ${{ steps.app-token.outputs.token }} 46 | release_url: ${{ steps.populate-release.outputs.release_url }} 47 | 48 | - name: "** Next Step **" 49 | if: ${{ success() }} 50 | run: | 51 | echo "Verify the final release" 52 | echo ${{ steps.finalize-release.outputs.release_url }} 53 | 54 | - name: "** Failure Message **" 55 | if: ${{ failure() }} 56 | run: | 57 | echo "Failed to Publish the Draft Release Url:" 58 | echo ${{ steps.populate-release.outputs.release_url }} 59 | -------------------------------------------------------------------------------- /packages/labextension/schema/topbar-item.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Resource Usage Indicator", 3 | "description": "Resource Usage Indicator settings.", 4 | "jupyter.lab.toolbars": { 5 | "TopBar": [ 6 | { 7 | "name": "cpu", 8 | "rank": 120 9 | }, 10 | { 11 | "name": "memory", 12 | "rank": 130 13 | }, 14 | { 15 | "name": "disk", 16 | "rank": 140 17 | } 18 | ] 19 | }, 20 | "properties": { 21 | "enable": { 22 | "title": "Enable resource usage indicators", 23 | "description": "Whether to enable resource usage indicators on topbar", 24 | "default": false, 25 | "type": "boolean" 26 | }, 27 | "refreshRate": { 28 | "title": "Refresh Rate (ms)", 29 | "description": "Refresh Rate to sync metrics data", 30 | "default": 5000, 31 | "type": "number" 32 | }, 33 | "memory": { 34 | "title": "Memory Settings", 35 | "description": "Settings for the memory indicator", 36 | "$ref": "#/definitions/memory", 37 | "default": { 38 | "label": "| Mem: " 39 | } 40 | }, 41 | "cpu": { 42 | "title": "CPU Settings", 43 | "description": "Settings for the CPU indicator", 44 | "$ref": "#/definitions/cpu", 45 | "default": { 46 | "label": "CPU: " 47 | } 48 | }, 49 | "disk": { 50 | "title": "Disk Settings", 51 | "description": "Settings for the Disk indicator", 52 | "$ref": "#/definitions/disk", 53 | "default": { 54 | "label": "| Disk: " 55 | } 56 | } 57 | }, 58 | "additionalProperties": false, 59 | "definitions": { 60 | "memory": { 61 | "type": "object", 62 | "properties": { 63 | "label": { 64 | "type": "string", 65 | "description": "Label for the memory indicator" 66 | } 67 | } 68 | }, 69 | "cpu": { 70 | "type": "object", 71 | "properties": { 72 | "label": { 73 | "type": "string", 74 | "description": "Label for the cpu indicator" 75 | } 76 | } 77 | }, 78 | "disk": { 79 | "type": "object", 80 | "properties": { 81 | "label": { 82 | "type": "string", 83 | "description": "Label for the disk indicator" 84 | } 85 | } 86 | } 87 | }, 88 | "type": "object" 89 | } 90 | -------------------------------------------------------------------------------- /jupyter_resource_usage/tests/test_basic.py: -------------------------------------------------------------------------------- 1 | from mock import MagicMock 2 | from mock import patch 3 | 4 | 5 | class TestBasic: 6 | """Some basic tests, checking import, making sure APIs remain consistent, etc""" 7 | 8 | def test_import_serverextension(self): 9 | """Check that serverextension hooks are available""" 10 | from jupyter_resource_usage import ( 11 | _jupyter_server_extension_points, 12 | _jupyter_nbextension_paths, 13 | load_jupyter_server_extension, 14 | ) 15 | 16 | assert _jupyter_server_extension_points() == [ 17 | {"module": "jupyter_resource_usage"} 18 | ] 19 | assert _jupyter_nbextension_paths() == [ 20 | { 21 | "section": "notebook", 22 | "dest": "jupyter_resource_usage", 23 | "src": "static", 24 | "require": "jupyter_resource_usage/main", 25 | } 26 | ] 27 | 28 | # mock a notebook app 29 | nbapp_mock = MagicMock() 30 | nbapp_mock.web_app.settings = {"base_url": ""} 31 | 32 | # mock these out for unit test 33 | with ( 34 | patch("tornado.ioloop.PeriodicCallback") as periodic_callback_mock, 35 | patch( 36 | "jupyter_resource_usage.server_extension.ResourceUseDisplay" 37 | ) as resource_use_display_mock, 38 | patch( 39 | "jupyter_resource_usage.server_extension.PrometheusHandler" 40 | ) as prometheus_handler_mock, 41 | patch( 42 | "jupyter_resource_usage.server_extension.PSUtilMetricsLoader" 43 | ) as psutil_metrics_loader, 44 | ): 45 | # load up with mock 46 | load_jupyter_server_extension(nbapp_mock) 47 | 48 | # assert that we installed the application in settings 49 | print(nbapp_mock.web_app.settings) 50 | assert ( 51 | "jupyter_resource_usage_display_config" in nbapp_mock.web_app.settings 52 | ) 53 | 54 | # assert that we instantiated a periodic callback with the fake 55 | # prometheus 56 | assert periodic_callback_mock.return_value.start.call_count == 1 57 | assert prometheus_handler_mock.call_count == 1 58 | prometheus_handler_mock.assert_called_with( 59 | psutil_metrics_loader(nbapp_mock) 60 | ) 61 | -------------------------------------------------------------------------------- /packages/labextension/src/resourceUsage.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jupyter Development Team. 2 | // Distributed under the terms of the Modified BSD License. 3 | 4 | import { VDomRenderer } from '@jupyterlab/apputils'; 5 | 6 | import { TextItem } from '@jupyterlab/statusbar'; 7 | 8 | import { TranslationBundle } from '@jupyterlab/translation'; 9 | 10 | import React from 'react'; 11 | 12 | import { ResourceUsage } from './model'; 13 | 14 | import { resourceItem } from './text'; 15 | 16 | /** 17 | * A VDomRenderer for showing resource usage by a kernel. 18 | */ 19 | export class ResourceUsageStatus extends VDomRenderer { 20 | /** 21 | * Construct a new resource usage status item. 22 | */ 23 | constructor(trans: TranslationBundle, options: ResourceUsage.Model.IOptions) { 24 | super(new ResourceUsage.Model(options)); 25 | this._trans = trans; 26 | } 27 | 28 | /** 29 | * Render the resource usage status item. 30 | */ 31 | render(): JSX.Element { 32 | if (!this.model) { 33 | return
; 34 | } 35 | let text: string; 36 | if (this.model.memoryLimit === null) { 37 | text = this._trans.__( 38 | '%1 %2 %3', 39 | this.model.memLabel, 40 | this.model.currentMemory.toFixed(Private.DECIMAL_PLACES), 41 | this.model.memUnits 42 | ); 43 | } else { 44 | text = this._trans.__( 45 | '%1 %2 / %3 %4', 46 | this.model.memLabel, 47 | this.model.currentMemory.toFixed(Private.DECIMAL_PLACES), 48 | this.model.memoryLimit.toFixed(Private.DECIMAL_PLACES), 49 | this.model.memUnits 50 | ); 51 | } 52 | if (this.model.cpuAvailable) { 53 | text = `${this.model.cpuLabel} ${( 54 | this.model.currentCpuPercent * 100 55 | ).toFixed(Private.DECIMAL_PLACES)} % | ${text}`; 56 | } 57 | if (this.model.diskAvailable) { 58 | text = `${this.model.diskLabel} ${this.model.currentDisk.toFixed( 59 | Private.DECIMAL_PLACES 60 | )} / ${this.model.maxDisk.toFixed(Private.DECIMAL_PLACES)} ${ 61 | this.model.diskUnits 62 | } | ${text}`; 63 | } 64 | if (!this.model.usageWarnings.hasWarning) { 65 | return ( 66 | 70 | ); 71 | } else { 72 | return ( 73 | 78 | ); 79 | } 80 | } 81 | 82 | private _trans: TranslationBundle; 83 | } 84 | 85 | /** 86 | * A namespace for module private statics. 87 | */ 88 | namespace Private { 89 | /** 90 | * The number of decimal places to use when rendering memory usage. 91 | */ 92 | export const DECIMAL_PLACES = 2; 93 | } 94 | -------------------------------------------------------------------------------- /packages/labextension/src/tracker.ts: -------------------------------------------------------------------------------- 1 | import { ILabShell } from '@jupyterlab/application'; 2 | import { INotebookTracker } from '@jupyterlab/notebook'; 3 | import { IConsoleTracker } from '@jupyterlab/console'; 4 | import { ISignal, Signal } from '@lumino/signaling'; 5 | import { IWidgetWithSession, hasKernelSession } from './types'; 6 | 7 | /** 8 | * Tracks widgets with kernels as well as possible given the available tokens. 9 | */ 10 | export class KernelWidgetTracker { 11 | constructor(options: KernelWidgetTracker.IOptions) { 12 | const { labShell, notebookTracker, consoleTracker } = options; 13 | this._currentChanged = new Signal(this); 14 | if (labShell) { 15 | labShell.currentChanged.connect((_, update) => { 16 | const widget = update.newValue; 17 | if (widget && hasKernelSession(widget)) { 18 | this._currentChanged.emit(widget); 19 | this._currentWidget = widget; 20 | } else { 21 | this._currentChanged.emit(null); 22 | this._currentWidget = null; 23 | } 24 | }); 25 | } else { 26 | notebookTracker.currentChanged.connect((_, widget) => { 27 | this._currentChanged.emit(widget); 28 | this._currentWidget = widget; 29 | }); 30 | if (consoleTracker) { 31 | consoleTracker.currentChanged.connect((_, widget) => { 32 | this._currentChanged.emit(widget); 33 | this._currentWidget = widget; 34 | }); 35 | } 36 | } 37 | // handle an existing current widget in case the KernelWidgetTracker 38 | // is created a bit later, or if there is already a Notebook widget available 39 | // on page load like in Notebook 7. 40 | if (labShell?.currentWidget && hasKernelSession(labShell?.currentWidget)) { 41 | this._currentWidget = labShell.currentWidget; 42 | } else { 43 | this._currentWidget = 44 | notebookTracker.currentWidget ?? consoleTracker?.currentWidget ?? null; 45 | } 46 | } 47 | 48 | /** 49 | * Emits on any change of active widget. The value is a known widget with 50 | * kernel session or null if user switched to a widget which does not support 51 | * kernel sessions. 52 | */ 53 | get currentChanged(): ISignal< 54 | KernelWidgetTracker, 55 | IWidgetWithSession | null 56 | > { 57 | return this._currentChanged; 58 | } 59 | 60 | get currentWidget(): IWidgetWithSession | null { 61 | return this._currentWidget; 62 | } 63 | 64 | private _currentChanged: Signal< 65 | KernelWidgetTracker, 66 | IWidgetWithSession | null 67 | >; 68 | 69 | private _currentWidget: IWidgetWithSession | null = null; 70 | } 71 | 72 | /** 73 | * Namespace for kernel widget tracker. 74 | */ 75 | export namespace KernelWidgetTracker { 76 | export interface IOptions { 77 | notebookTracker: INotebookTracker; 78 | labShell: ILabShell | null; 79 | consoleTracker: IConsoleTracker | null; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /packages/labextension/src/indicator.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, ReactElement } from 'react'; 2 | 3 | import { Sparklines, SparklinesLine, SparklinesSpots } from 'react-sparklines'; 4 | 5 | /** 6 | * A indicator fill component. 7 | */ 8 | const IndicatorFiller = ({ 9 | percentage, 10 | color, 11 | }: { 12 | percentage: number; 13 | color: string; 14 | }): ReactElement => { 15 | return ( 16 |
23 | ); 24 | }; 25 | 26 | /** 27 | * An indicator bar component. 28 | */ 29 | const IndicatorBar = ({ 30 | values, 31 | percentage, 32 | baseColor, 33 | }: { 34 | values: number[]; 35 | percentage: number; 36 | baseColor: string; 37 | }): ReactElement => { 38 | const [isSparklines, setIsSparklines] = useState(false); 39 | 40 | const toggleSparklines = (): void => { 41 | setIsSparklines(!isSparklines); 42 | }; 43 | 44 | const color = 45 | percentage > 0.5 ? (percentage > 0.8 ? 'red' : 'orange') : baseColor; 46 | 47 | return ( 48 |
toggleSparklines()}> 49 | {isSparklines && ( 50 | 57 | 65 | 66 | 67 | )} 68 | {!isSparklines && ( 69 | 70 | )} 71 |
72 | ); 73 | }; 74 | 75 | /** 76 | * An incicator component for displaying resource usage. 77 | * 78 | */ 79 | export const IndicatorComponent = ({ 80 | enabled, 81 | values, 82 | label, 83 | color, 84 | text, 85 | }: { 86 | enabled: boolean; 87 | values: number[]; 88 | label: string; 89 | color: string; 90 | text: string; 91 | }): ReactElement => { 92 | const percentage = values[values.length - 1]; 93 | return ( 94 | <> 95 | {enabled && ( 96 |
97 |
{label}
98 | {percentage !== null && ( 99 |
100 | 105 |
106 | )} 107 |
{text}
108 |
109 | )} 110 | 111 | ); 112 | }; 113 | -------------------------------------------------------------------------------- /jupyter_resource_usage/prometheus.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from prometheus_client import Gauge 4 | 5 | from jupyter_resource_usage.metrics import PSUtilMetricsLoader 6 | 7 | try: 8 | # Traitlets >= 4.3.3 9 | from traitlets import Callable 10 | except ImportError: 11 | from .utils import Callable 12 | 13 | 14 | class PrometheusHandler(Callable): 15 | def __init__(self, metricsloader: PSUtilMetricsLoader): 16 | super().__init__() 17 | self.metricsloader = metricsloader 18 | self.config = metricsloader.config 19 | self.session_manager = metricsloader.server_app.session_manager 20 | 21 | gauge_names = [ 22 | "total_memory", 23 | "max_memory", 24 | "total_cpu", 25 | "max_cpu", 26 | "max_disk", 27 | "current_disk", 28 | ] 29 | for name in gauge_names: 30 | phrase = name + "_usage" 31 | gauge = Gauge(phrase, "counter for " + phrase.replace("_", " "), []) 32 | setattr(self, phrase.upper(), gauge) 33 | 34 | async def __call__(self, *args, **kwargs): 35 | memory_metric_values = self.metricsloader.memory_metrics() 36 | if memory_metric_values is not None: 37 | self.TOTAL_MEMORY_USAGE.set(memory_metric_values["memory_info_rss"]) 38 | self.MAX_MEMORY_USAGE.set(self.apply_memory_limit(memory_metric_values)) 39 | if self.config.track_cpu_percent: 40 | cpu_metric_values = self.metricsloader.cpu_metrics() 41 | if cpu_metric_values is not None: 42 | self.TOTAL_CPU_USAGE.set(cpu_metric_values["cpu_percent"]) 43 | self.MAX_CPU_USAGE.set(self.apply_cpu_limit(cpu_metric_values)) 44 | if self.config.track_disk_usage: 45 | disk_metric_values = self.metricsloader.disk_metrics() 46 | if disk_metric_values is not None: 47 | self.CURRENT_DISK_USAGE.set(disk_metric_values["disk_usage_used"]) 48 | self.MAX_DISK_USAGE.set(disk_metric_values["disk_usage_total"]) 49 | 50 | def apply_memory_limit(self, memory_metric_values) -> Optional[int]: 51 | if memory_metric_values is None: 52 | return None 53 | else: 54 | if callable(self.config.mem_limit): 55 | return self.config.mem_limit( 56 | rss=memory_metric_values["memory_info_rss"] 57 | ) 58 | elif self.config.mem_limit > 0: # mem_limit is an Int 59 | return self.config.mem_limit 60 | else: 61 | return memory_metric_values["virtual_memory_total"] 62 | 63 | def apply_cpu_limit(self, cpu_metric_values) -> Optional[float]: 64 | if cpu_metric_values is None: 65 | return None 66 | else: 67 | if callable(self.config.cpu_limit): 68 | return self.config.cpu_limit( 69 | cpu_percent=cpu_metric_values["cpu_percent"] 70 | ) 71 | elif self.config.cpu_limit > 0.0: # cpu_limit is a Float 72 | return self.config.cpu_limit 73 | else: 74 | return 100.0 * cpu_metric_values["cpu_count"] 75 | -------------------------------------------------------------------------------- /packages/labextension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jupyter-server/resource-usage", 3 | "version": "1.2.0", 4 | "description": "JupyterLab extension to add resource usage UI items", 5 | "keywords": [ 6 | "jupyter", 7 | "jupyterlab", 8 | "jupyterlab-extension" 9 | ], 10 | "homepage": "https://github.com/jupyter-server/jupyter-resource-usage", 11 | "bugs": { 12 | "url": "https://github.com/jupyter-server/jupyter-resource-usage/issues" 13 | }, 14 | "license": "BSD-3-Clause", 15 | "author": "Jupyter Development Team", 16 | "files": [ 17 | "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", 18 | "style/**/*.{css,eot,gif,html,jpg,json,png,svg,woff2,ttf}" 19 | ], 20 | "main": "lib/index.js", 21 | "types": "lib/index.d.ts", 22 | "style": "style/index.css", 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/jupyter-server/jupyter-resource-usage.git" 26 | }, 27 | "scripts": { 28 | "build": "jlpm run build:lib && jlpm run build:labextension:dev", 29 | "build:prod": "jlpm run build:lib && jlpm run build:labextension", 30 | "build:labextension": "jupyter labextension build .", 31 | "build:labextension:dev": "jupyter labextension build --development True .", 32 | "build:lib": "tsc", 33 | "clean": "jlpm run clean:lib", 34 | "clean:lib": "rimraf lib tsconfig.tsbuildinfo", 35 | "clean:labextension": "rimraf ../../jupyter_resource_usage/labextension", 36 | "clean:all": "jlpm run clean:lib && jlpm run clean:labextension", 37 | "eslint": "eslint . --ext .ts,.tsx --fix", 38 | "eslint:check": "eslint . --ext .ts,.tsx", 39 | "install:extension": "jupyter labextension develop --overwrite .", 40 | "watch": "run-p watch:src watch:labextension", 41 | "watch:src": "tsc -w", 42 | "watch:labextension": "jupyter labextension watch ." 43 | }, 44 | "dependencies": { 45 | "@jupyterlab/application": "^4.0.0", 46 | "@jupyterlab/apputils": "^4.0.0", 47 | "@jupyterlab/console": "^4.0.0", 48 | "@jupyterlab/coreutils": "^6.0.0", 49 | "@jupyterlab/notebook": "^4.0.0", 50 | "@jupyterlab/services": "^7.0.0", 51 | "@jupyterlab/statusbar": "^4.0.0", 52 | "@jupyterlab/translation": "^4.0.0", 53 | "@lumino/polling": "^2.1.1", 54 | "react-sparklines": "^1.7.0", 55 | "typestyle": "^2.4.0" 56 | }, 57 | "devDependencies": { 58 | "@jupyterlab/builder": "^4.0.0", 59 | "@types/react-sparklines": "^1.7.0", 60 | "@typescript-eslint/eslint-plugin": "^4.8.1", 61 | "@typescript-eslint/parser": "^4.8.1", 62 | "eslint": "^7.14.0", 63 | "eslint-config-prettier": "^6.15.0", 64 | "eslint-plugin-prettier": "^3.1.4", 65 | "mkdirp": "^1.0.3", 66 | "npm-run-all": "^4.1.5", 67 | "prettier": "^2.1.1", 68 | "rimraf": "^3.0.2", 69 | "typescript": "~5.0.0" 70 | }, 71 | "sideEffects": [ 72 | "style/*.css", 73 | "style/index.js" 74 | ], 75 | "styleModule": "style/index.js", 76 | "publishConfig": { 77 | "access": "public" 78 | }, 79 | "jupyterlab": { 80 | "discovery": { 81 | "server": { 82 | "managers": [ 83 | "pip" 84 | ], 85 | "base": { 86 | "name": "jupyterlab_kernel_usage" 87 | } 88 | } 89 | }, 90 | "extension": true, 91 | "schemaDir": "schema", 92 | "outputDir": "../../jupyter_resource_usage/labextension" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jupyter-server/jupyter-resource-usage-root", 3 | "private": true, 4 | "version": "0.1.0", 5 | "license": "BSD-3-Clause", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/jupyter-server/jupyter-resource-usage.git" 9 | }, 10 | "workspaces": { 11 | "packages": [ 12 | "packages/*" 13 | ] 14 | }, 15 | "scripts": { 16 | "build": "lerna run build", 17 | "build:prod": "lerna run build:prod", 18 | "clean": "lerna run clean", 19 | "install": "lerna bootstrap", 20 | "install:dev": "jlpm run build:prod && jlpm run develop", 21 | "install:extension": "jupyter labextension develop --overwrite .", 22 | "eslint": "eslint . --ext .ts,.tsx --fix", 23 | "eslint:check": "eslint . --ext .ts,.tsx", 24 | "lint": "jlpm run eslint && jlpm run prettier", 25 | "lint:check": "jlpm run eslint:check && jlpm run prettier:check", 26 | "prettier": "prettier --write \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", 27 | "prettier:check": "prettier --list-different \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", 28 | "publish": "jlpm run clean && jlpm run build && lerna publish", 29 | "test": "lerna run test", 30 | "watch": "lerna run watch" 31 | }, 32 | "devDependencies": { 33 | "@typescript-eslint/eslint-plugin": "^4.32.0", 34 | "@typescript-eslint/parser": "^4.32.0", 35 | "eslint": "^7.32.0", 36 | "eslint-config-prettier": "^8.3.0", 37 | "eslint-plugin-prettier": "^4.0.0", 38 | "lerna": "^4.0.0", 39 | "lint-staged": "^10.4.0", 40 | "npm-run-all": "^4.1.5", 41 | "prettier": "^2.4.1", 42 | "rimraf": "^3.0.2" 43 | }, 44 | "resolutions": { 45 | "@types/react": "18.2.16", 46 | "parse-url": "8.1.0", 47 | "parse-path": "5.0.0", 48 | "lodash": "4.17.21" 49 | }, 50 | "eslintIgnore": [ 51 | "**/*.d.ts", 52 | "dist", 53 | "*node_modules*", 54 | "coverage", 55 | "tests" 56 | ], 57 | "prettier": { 58 | "singleQuote": true 59 | }, 60 | "eslintConfig": { 61 | "extends": [ 62 | "eslint:recommended", 63 | "plugin:@typescript-eslint/eslint-recommended", 64 | "plugin:@typescript-eslint/recommended", 65 | "plugin:prettier/recommended" 66 | ], 67 | "parser": "@typescript-eslint/parser", 68 | "plugins": [ 69 | "@typescript-eslint" 70 | ], 71 | "rules": { 72 | "@typescript-eslint/naming-convention": [ 73 | "error", 74 | { 75 | "selector": "interface", 76 | "format": [ 77 | "PascalCase" 78 | ], 79 | "custom": { 80 | "regex": "^I[A-Z]", 81 | "match": true 82 | } 83 | } 84 | ], 85 | "@typescript-eslint/no-unused-vars": [ 86 | "warn", 87 | { 88 | "args": "none" 89 | } 90 | ], 91 | "@typescript-eslint/no-explicit-any": "off", 92 | "@typescript-eslint/no-namespace": "off", 93 | "@typescript-eslint/no-use-before-define": "off", 94 | "@typescript-eslint/quotes": [ 95 | "error", 96 | "single", 97 | { 98 | "avoidEscape": true, 99 | "allowTemplateLiterals": false 100 | } 101 | ], 102 | "curly": [ 103 | "error", 104 | "all" 105 | ], 106 | "eqeqeq": "error", 107 | "prefer-arrow-callback": "error" 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /jupyter_resource_usage/metrics.py: -------------------------------------------------------------------------------- 1 | try: 2 | import psutil 3 | except ImportError: 4 | psutil = None 5 | 6 | from jupyter_server.serverapp import ServerApp 7 | 8 | 9 | class PSUtilMetricsLoader: 10 | def __init__(self, server_app: ServerApp): 11 | self.config = server_app.web_app.settings[ 12 | "jupyter_resource_usage_display_config" 13 | ] 14 | self.server_app = server_app 15 | 16 | def get_process_metric_value(self, process, name, args, kwargs, attribute=None): 17 | try: 18 | # psutil.Process methods will either return... 19 | metric_value = getattr(process, name)(*args, **kwargs) 20 | if attribute is not None: # ... a named tuple 21 | return getattr(metric_value, attribute) 22 | else: # ... or a number 23 | return metric_value 24 | # Avoid littering logs with stack traces 25 | # complaining about dead processes 26 | except BaseException: 27 | return 0 28 | 29 | def process_metric(self, name, args=[], kwargs={}, attribute=None): 30 | if psutil is None: 31 | return None 32 | else: 33 | current_process = psutil.Process() 34 | all_processes = [current_process] + current_process.children(recursive=True) 35 | 36 | process_metric_value = lambda process: self.get_process_metric_value( 37 | process, name, args, kwargs, attribute 38 | ) 39 | 40 | return sum([process_metric_value(process) for process in all_processes]) 41 | 42 | def system_metric(self, name, args=[], kwargs={}, attribute=None): 43 | if psutil is None: 44 | return None 45 | else: 46 | # psutil functions will either raise an error, or return... 47 | try: 48 | metric_value = getattr(psutil, name)(*args, **kwargs) 49 | except: 50 | return None 51 | if attribute is not None: # ... a named tuple 52 | return getattr(metric_value, attribute) 53 | else: # ... or a number 54 | return metric_value 55 | 56 | def get_metric_values(self, metrics, metric_type): 57 | metric_types = {"process": self.process_metric, "system": self.system_metric} 58 | metric_value = metric_types[metric_type] # Switch statement 59 | 60 | metric_values = {} 61 | for metric in metrics: 62 | name = metric["name"] 63 | if metric.get("attribute", False): 64 | name += "_" + metric.get("attribute") 65 | metric_values.update({name: metric_value(**metric)}) 66 | return metric_values 67 | 68 | def metrics(self, process_metrics, system_metrics): 69 | metric_values = {} 70 | if process_metrics: 71 | metric_values.update(self.get_metric_values(process_metrics, "process")) 72 | if system_metrics: 73 | metric_values.update(self.get_metric_values(system_metrics, "system")) 74 | 75 | if any(value is None for value in metric_values.values()): 76 | return None 77 | 78 | return metric_values 79 | 80 | def memory_metrics(self): 81 | return self.metrics( 82 | self.config.process_memory_metrics, self.config.system_memory_metrics 83 | ) 84 | 85 | def cpu_metrics(self): 86 | return self.metrics( 87 | self.config.process_cpu_metrics, self.config.system_cpu_metrics 88 | ) 89 | 90 | def disk_metrics(self): 91 | return self.metrics( 92 | self.config.process_disk_metrics, self.config.system_disk_metrics 93 | ) 94 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "hatchling>=1.5.0", 4 | "jupyterlab>=4.0,<5", 5 | "pip", 6 | ] 7 | build-backend = "hatchling.build" 8 | 9 | [project] 10 | name = "jupyter-resource-usage" 11 | description = "Jupyter Extension to show resource usage" 12 | readme = "README.md" 13 | requires-python = ">=3.9" 14 | authors = [ 15 | { name = "Jupyter Development Team" }, 16 | ] 17 | keywords = [ 18 | "IPython", 19 | "Jupyter", 20 | "JupyterLab", 21 | ] 22 | classifiers = [ 23 | "Framework :: Jupyter", 24 | "Framework :: Jupyter :: JupyterLab", 25 | "Framework :: Jupyter :: JupyterLab :: 4", 26 | "Framework :: Jupyter :: JupyterLab :: Extensions", 27 | "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", 28 | "Intended Audience :: Developers", 29 | "Intended Audience :: Science/Research", 30 | "License :: OSI Approved :: BSD License", 31 | "Programming Language :: Python", 32 | "Programming Language :: Python :: 3", 33 | "Programming Language :: Python :: 3.9", 34 | "Programming Language :: Python :: 3.10", 35 | "Programming Language :: Python :: 3.11", 36 | "Programming Language :: Python :: 3.12", 37 | "Programming Language :: Python :: 3.13", 38 | ] 39 | dependencies = [ 40 | "jupyter_server>=2.0", 41 | "prometheus_client", 42 | "psutil>=5.6", 43 | "pyzmq>=19", 44 | ] 45 | dynamic = ["version"] 46 | 47 | [project.license] 48 | file = "LICENSE" 49 | 50 | [project.optional-dependencies] 51 | dev = [ 52 | "autopep8", 53 | "black", 54 | "flake8", 55 | "mock", 56 | "pytest", 57 | "pytest-cov>=2.6.1", 58 | ] 59 | 60 | [project.urls] 61 | Homepage = "https://github.com/jupyter-server/jupyter-resource-usage" 62 | 63 | [tool.hatch.build.targets.wheel.shared-data] 64 | "jupyter_resource_usage/static" = "share/jupyter/nbextensions/jupyter_resource_usage" 65 | "jupyter_resource_usage/labextension" = "share/jupyter/labextensions/@jupyter-server/resource-usage" 66 | "install.json" = "share/jupyter/labextensions/@jupyter-server/resource-usage/install.json" 67 | "jupyter-config/jupyter_server_config.d" = "etc/jupyter/jupyter_server_config.d" 68 | "jupyter-config/jupyter_notebook_config.d" = "etc/jupyter/jupyter_notebook_config.d" 69 | "jupyter-config/nbconfig/notebook.d" = "etc/jupyter/nbconfig/notebook.d" 70 | 71 | [tool.hatch.version] 72 | path = "jupyter_resource_usage/_version.py" 73 | 74 | [tool.hatch.build.targets.sdist] 75 | artifacts = ["jupyter_resource_usage/labextension"] 76 | exclude = [".github", "binder"] 77 | 78 | [tool.hatch.build.hooks.jupyter-builder] 79 | dependencies = [ 80 | "hatch-jupyter-builder>=0.8.2", 81 | ] 82 | build-function = "hatch_jupyter_builder.npm_builder" 83 | ensured-targets = [ 84 | "jupyter_resource_usage/labextension/static/style.js", 85 | "jupyter_resource_usage/labextension/package.json", 86 | ] 87 | skip-if-exists = [ 88 | "jupyter_resource_usage/labextension/static/style.js", 89 | ] 90 | 91 | [tool.hatch.build.hooks.jupyter-builder.build-kwargs] 92 | build_cmd = "build:prod" 93 | npm = [ 94 | "jlpm", 95 | ] 96 | 97 | [tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] 98 | build_cmd = "build" 99 | npm = [ 100 | "jlpm", 101 | ] 102 | 103 | [tool.tbump.version] 104 | current = "1.2.0" 105 | regex = ''' 106 | (?P\d+)\.(?P\d+)\.(?P\d+) 107 | ((?Pa|b|rc|.dev)(?P\d+))? 108 | ''' 109 | 110 | [tool.tbump.git] 111 | message_template = "Bump to {new_version}" 112 | tag_template = "v{new_version}" 113 | 114 | [[tool.tbump.file]] 115 | src = "jupyter_resource_usage/_version.py" 116 | 117 | [[tool.tbump.file]] 118 | src = "packages/labextension/package.json" 119 | 120 | [[tool.tbump.field]] 121 | name = "channel" 122 | default = "" 123 | 124 | [[tool.tbump.field]] 125 | name = "release" 126 | default = "" 127 | 128 | [tool.jupyter-releaser.hooks] 129 | before-build-npm = [ 130 | "python -m pip install 'jupyterlab>=4.0,<5'", 131 | "jlpm", 132 | "jlpm clean", 133 | "jlpm build:prod", 134 | ] 135 | 136 | [tool.check-wheel-contents] 137 | ignore = ["W002", "W004"] 138 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions to jupyter-resource-usage are highly welcome! As a [Jupyter](https://jupyter.org) project, 4 | you can follow the [Jupyter contributor guide](https://docs.jupyter.org/en/latest/contributing/content-contributor.html). 5 | 6 | Make sure to also follow [Project Jupyter's Code of Conduct](https://github.com/jupyter/governance/blob/master/conduct/code_of_conduct.md) 7 | for a friendly and welcoming collaborative environment. 8 | 9 | ## Setting up a development environment 10 | 11 | We recommend using [pipenv](https://docs.pipenv.org/) to make development easier. 12 | 13 | Alternatively, you can also use `conda` or `mamba` to create new virtual environments. 14 | 15 | Clone the git repository: 16 | 17 | ```bash 18 | git clone https://github.com/jupyter-server/jupyter-resource-usage 19 | ``` 20 | 21 | Create an environment that will hold our dependencies: 22 | 23 | ```bash 24 | cd jupyter-resource-usage 25 | pipenv --python 3.6 26 | ``` 27 | 28 | With conda: 29 | 30 | ```bash 31 | conda create -n jupyter-resource-usage -c conda-forge python 32 | ``` 33 | 34 | Activate the virtual environment that pipenv created for us 35 | 36 | ```bash 37 | pipenv shell 38 | ``` 39 | 40 | With conda: 41 | 42 | ```bash 43 | conda activate jupyter-resource-usage 44 | ``` 45 | 46 | Do a dev install of jupyter-resource-usage and its dependencies 47 | 48 | ```bash 49 | pip install --editable .[dev] 50 | ``` 51 | 52 | Enable the server extension: 53 | 54 | ```bash 55 | jupyter serverextension enable --py jupyter_resource_usage --sys-prefix 56 | ``` 57 | 58 | _Note: if you're using Jupyter Server:_ 59 | 60 | ```bash 61 | jupyter server extension enable --py jupyter_resource_usage --sys-prefix 62 | ``` 63 | 64 | ## Classic notebook extension 65 | 66 | Install and enable the nbextension for use with Jupyter Classic Notebook. 67 | 68 | ```bash 69 | jupyter nbextension install --py jupyter_resource_usage --symlink --sys-prefix 70 | jupyter nbextension enable --py jupyter_resource_usage --sys-prefix 71 | ``` 72 | 73 | Start a Jupyter Notebook instance, open a new notebook and check out the memory usage in the top right! 74 | 75 | ```bash 76 | jupyter notebook 77 | ``` 78 | 79 | If you want to test the memory limit display functionality, you can do so by setting the `MEM_LIMIT` environment variable (in bytes) when starting `jupyter notebook`. 80 | 81 | ```bash 82 | MEM_LIMIT=$(expr 128 \* 1024 \* 1024) jupyter notebook 83 | ``` 84 | 85 | ## JupyterLab extension 86 | 87 | The JupyterLab extension for `jupyter-resource-usage` was bootstrapped from the [extension cookiecutter](https://github.com/jupyterlab/extension-cookiecutter-ts), and follows the common patterns and tooling for developing extensions. 88 | 89 | ```bash 90 | # activate the environment (conda, pipenv) 91 | 92 | # install the package in development mode 93 | python -m pip install -e ".[dev]" 94 | 95 | # link your development version of the extension with JupyterLab 96 | jupyter labextension develop . --overwrite 97 | 98 | # go to the labextension directory 99 | cd packages/labextension/ 100 | 101 | # Rebuild extension Typescript source after making changes 102 | jlpm run build 103 | ``` 104 | 105 | You can watch the source directory and run JupyterLab at the same time in different terminals to watch for changes in the extension's source and automatically rebuild the extension. 106 | 107 | ```bash 108 | # Watch the source directory in one terminal, automatically rebuilding when needed 109 | jlpm run watch 110 | # Run JupyterLab in another terminal 111 | jupyter lab 112 | ``` 113 | 114 | With the watch command running, every saved change will immediately be built locally and available in your running JupyterLab. Refresh JupyterLab to load the change in your browser (you may need to wait several seconds for the extension to be rebuilt). 115 | 116 | To check the extension is correctly installed, run: 117 | 118 | ```bash 119 | jupyter labextension list 120 | ``` 121 | 122 | It should show something like the following: 123 | 124 | ```bash 125 | JupyterLab v3.0.0 126 | /path/to/env/share/jupyter/labextensions 127 | jupyter-resource-usage v0.1.0 enabled OK 128 | ``` 129 | 130 | ## Which code creates what content 131 | 132 | The stats are created by the server-side code in `jupyter_resource_usage`. 133 | 134 | For the jupyterlab 4 / notebook 7 UIs, the code in `packages/labextension` creates and writes the content for both the statusbar and the topbar. 135 | 136 | The topbar is defined in the schema, whilst the contents of the statusbar is driven purely by the labextension code.... and labels are defined by their appropriate `*View.tsx` file 137 | 138 | ## pre-commit 139 | 140 | `jupyter-resource-usage` has adopted automatic code formatting so you shouldn't need to worry too much about your code style. 141 | As long as your code is valid, 142 | the pre-commit hook should take care of how it should look. Here is how to set up pre-commit hooks for automatic code formatting, etc. 143 | 144 | ```bash 145 | pre-commit install 146 | ``` 147 | 148 | You can also invoke the pre-commit hook manually at any time with 149 | 150 | ```bash 151 | pre-commit run 152 | ``` 153 | 154 | which should run any autoformatting on your code 155 | and tell you about any errors it couldn't fix automatically. 156 | You may also install [black integration](https://github.com/ambv/black#editor-integration) 157 | into your text editor to format code automatically. 158 | 159 | If you have already committed files before setting up the pre-commit 160 | hook with `pre-commit install`, you can fix everything up using 161 | `pre-commit run --all-files`. You need to make the fixing commit 162 | yourself after that. 163 | 164 | ## Tests 165 | 166 | It's a good idea to write tests to exercise any new features, 167 | or that trigger any bugs that you have fixed to catch regressions. `pytest` is used to run the test suite. You can run the tests with in the repo directory: 168 | 169 | ```bash 170 | python -m pytest -vvv jupyter_resource_usage 171 | ``` 172 | -------------------------------------------------------------------------------- /jupyter_resource_usage/api.py: -------------------------------------------------------------------------------- 1 | import json 2 | from concurrent.futures import ThreadPoolExecutor 3 | from inspect import isawaitable 4 | 5 | import psutil 6 | import zmq.asyncio 7 | from jupyter_client.jsonutil import date_default 8 | from jupyter_server.base.handlers import APIHandler 9 | from packaging import version 10 | from tornado import web 11 | from tornado.concurrent import run_on_executor 12 | 13 | 14 | try: 15 | import ipykernel 16 | 17 | IPYKERNEL_VERSION = ipykernel.__version__ 18 | USAGE_IS_SUPPORTED = version.parse("6.9.0") <= version.parse(IPYKERNEL_VERSION) 19 | except ImportError: 20 | USAGE_IS_SUPPORTED = False 21 | IPYKERNEL_VERSION = None 22 | 23 | 24 | class ApiHandler(APIHandler): 25 | executor = ThreadPoolExecutor(max_workers=5) 26 | 27 | @web.authenticated 28 | async def get(self): 29 | """ 30 | Calculate and return current resource usage metrics 31 | """ 32 | config = self.settings["jupyter_resource_usage_display_config"] 33 | 34 | cur_process = psutil.Process() 35 | all_processes = [cur_process] + cur_process.children(recursive=True) 36 | 37 | # Get memory information 38 | rss = 0 39 | pss = None 40 | for p in all_processes: 41 | try: 42 | info = p.memory_full_info() 43 | if hasattr(info, "pss"): 44 | pss = (pss or 0) + info.pss 45 | rss += info.rss 46 | except (psutil.NoSuchProcess, psutil.AccessDenied) as e: 47 | pass 48 | 49 | if callable(config.mem_limit): 50 | mem_limit = config.mem_limit(rss=rss, pss=pss) 51 | else: # mem_limit is an Int 52 | mem_limit = config.mem_limit 53 | 54 | limits = {"memory": {"rss": mem_limit, "pss": mem_limit}} 55 | if config.mem_limit and config.mem_warning_threshold != 0: 56 | limits["memory"]["warn"] = (mem_limit - rss) < ( 57 | mem_limit * config.mem_warning_threshold 58 | ) 59 | 60 | metrics = {"rss": rss, "limits": limits} 61 | if pss is not None: 62 | metrics["pss"] = pss 63 | 64 | # Optionally get CPU information 65 | if config.track_cpu_percent: 66 | cpu_count = psutil.cpu_count() 67 | cpu_percent = await self._get_cpu_percent(all_processes) 68 | 69 | if config.cpu_limit != 0: 70 | limits["cpu"] = {"cpu": config.cpu_limit} 71 | if config.cpu_warning_threshold != 0: 72 | limits["cpu"]["warn"] = (config.cpu_limit - cpu_percent) < ( 73 | config.cpu_limit * config.cpu_warning_threshold 74 | ) 75 | 76 | metrics.update(cpu_percent=cpu_percent, cpu_count=cpu_count) 77 | 78 | # Optionally get Disk information 79 | if config.track_disk_usage: 80 | try: 81 | disk_info = psutil.disk_usage(config.disk_path) 82 | except Exception: 83 | pass 84 | else: 85 | metrics.update(disk_used=disk_info.used, disk_total=disk_info.total) 86 | limits["disk"] = {"disk": disk_info.total} 87 | if config.disk_warning_threshold != 0: 88 | limits["disk"]["warn"] = (disk_info.total - disk_info.used) < ( 89 | disk_info.total * config.disk_warning_threshold 90 | ) 91 | 92 | self.write(json.dumps(metrics)) 93 | 94 | @run_on_executor 95 | def _get_cpu_percent(self, all_processes): 96 | def get_cpu_percent(p): 97 | try: 98 | return p.cpu_percent() 99 | # Avoid littering logs with stack traces complaining 100 | # about dead processes having no CPU usage 101 | except: 102 | return 0 103 | 104 | return sum([get_cpu_percent(p) for p in all_processes]) 105 | 106 | 107 | class KernelUsageHandler(APIHandler): 108 | @web.authenticated 109 | async def get(self, matched_part=None, *args, **kwargs): 110 | if not USAGE_IS_SUPPORTED: 111 | self.write( 112 | json.dumps( 113 | { 114 | "content": { 115 | "reason": "not_supported", 116 | "kernel_version": IPYKERNEL_VERSION, 117 | } 118 | } 119 | ) 120 | ) 121 | return 122 | 123 | config = self.settings["jupyter_resource_usage_display_config"] 124 | 125 | kernel_id = matched_part 126 | km = self.kernel_manager 127 | lkm = km.pinned_superclass.get_kernel(km, kernel_id) 128 | session = lkm.session 129 | client = lkm.client() 130 | 131 | control_channel = client.control_channel 132 | usage_request = session.msg("usage_request", {}) 133 | control_channel.send(usage_request) 134 | poller = zmq.asyncio.Poller() 135 | control_socket = control_channel.socket 136 | poller.register(control_socket, zmq.POLLIN) 137 | timeout_ms = 10_000 138 | events = dict(await poller.poll(timeout_ms)) 139 | if control_socket not in events: 140 | out = json.dumps( 141 | { 142 | "content": {"reason": "timeout", "timeout_ms": timeout_ms}, 143 | "kernel_id": kernel_id, 144 | } 145 | ) 146 | 147 | else: 148 | res = client.control_channel.get_msg(timeout=0) 149 | if isawaitable(res): 150 | # control_channel.get_msg may return a Future, 151 | # depending on configured KernelManager class 152 | res = await res 153 | if res: 154 | res["kernel_id"] = kernel_id 155 | res["content"].update({"host_usage_flag": config.show_host_usage}) 156 | out = json.dumps(res, default=date_default) 157 | client.stop_channels() 158 | self.write(out) 159 | -------------------------------------------------------------------------------- /jupyter_resource_usage/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from traitlets import Bool 4 | from traitlets import default 5 | from traitlets import Dict 6 | from traitlets import Float 7 | from traitlets import Int 8 | from traitlets import List 9 | from traitlets import TraitType 10 | from traitlets import Unicode 11 | from traitlets import Union 12 | from traitlets.config import Configurable 13 | 14 | try: 15 | # Traitlets >= 4.3.3 16 | from traitlets import Callable 17 | except ImportError: 18 | from .utils import Callable 19 | 20 | 21 | class PSUtilMetric(TraitType): 22 | """A trait describing the format to specify a metric from the psutil package""" 23 | 24 | info_text = "A dictionary specifying the function/method name, any keyword arguments, and if a named tuple is returned, which attribute of the named tuple to select" 25 | 26 | def validate(self, obj, value): 27 | if isinstance(value, dict): 28 | keys = list(value.keys()) 29 | if "name" in keys: 30 | keys.remove("name") 31 | if all(key in ["args", "kwargs", "attribute"] for key in keys): 32 | return value 33 | self.error(obj, value) 34 | 35 | 36 | class ResourceUseDisplay(Configurable): 37 | """ 38 | Holds server-side configuration for jupyter-resource-usage 39 | """ 40 | 41 | # Needs to be defined early, so the metrics can use it. 42 | disk_path = Union( 43 | trait_types=[Unicode(), Callable()], 44 | default_value="/home/jovyan", 45 | help=""" 46 | A path in the partition to be reported on. 47 | """, 48 | ).tag(config=True) 49 | 50 | process_memory_metrics = List( 51 | trait=PSUtilMetric(), 52 | default_value=[{"name": "memory_info", "attribute": "rss"}], 53 | ) 54 | 55 | system_memory_metrics = List( 56 | trait=PSUtilMetric(), 57 | default_value=[{"name": "virtual_memory", "attribute": "total"}], 58 | ) 59 | 60 | process_cpu_metrics = List( 61 | trait=PSUtilMetric(), 62 | default_value=[{"name": "cpu_percent", "kwargs": {"interval": 0.05}}], 63 | ) 64 | 65 | system_cpu_metrics = List( 66 | trait=PSUtilMetric(), default_value=[{"name": "cpu_count"}] 67 | ) 68 | 69 | process_disk_metrics = List( 70 | trait=PSUtilMetric(), 71 | default_value=[], 72 | ) 73 | 74 | system_disk_metrics = List( 75 | trait=PSUtilMetric(), 76 | default_value=[ 77 | {"name": "disk_usage", "args": [disk_path], "attribute": "total"}, 78 | {"name": "disk_usage", "args": [disk_path], "attribute": "used"}, 79 | ], 80 | ) 81 | 82 | mem_warning_threshold = Float( 83 | default_value=0.1, 84 | help=""" 85 | Warn user with flashing lights when memory usage is within this fraction 86 | memory limit. 87 | 88 | For example, if memory limit is 128MB, `mem_warning_threshold` is 0.1, 89 | we will start warning the user when they use (128 - (128 * 0.1)) MB. 90 | 91 | Set to 0 to disable warning. 92 | """, 93 | ).tag(config=True) 94 | 95 | mem_limit = Union( 96 | trait_types=[Int(), Callable()], 97 | help=""" 98 | Memory limit to display to the user, in bytes. 99 | Can also be a function which calculates the memory limit. 100 | 101 | Note that this does not actually limit the user's memory usage! 102 | 103 | Defaults to reading from the `MEM_LIMIT` environment variable. If 104 | set to 0, the max memory available is displayed. 105 | """, 106 | ).tag(config=True) 107 | 108 | @default("mem_limit") 109 | def _mem_limit_default(self): 110 | return int(os.environ.get("MEM_LIMIT", 0)) 111 | 112 | track_cpu_percent = Bool( 113 | default_value=False, 114 | help=""" 115 | Set to True in order to enable reporting of CPU usage statistics. 116 | """, 117 | ).tag(config=True) 118 | 119 | cpu_warning_threshold = Float( 120 | default_value=0.1, 121 | help=""" 122 | Warn user with flashing lights when CPU usage is within this fraction 123 | CPU usage limit. 124 | 125 | For example, if CPU limit is 150%, `cpu_warning_threshold` is 0.1, 126 | we will start warning the user when they use (150 - (150 * 0.1)) %. 127 | 128 | Set to 0 to disable warning. 129 | """, 130 | ).tag(config=True) 131 | 132 | cpu_limit = Union( 133 | trait_types=[Float(), Callable()], 134 | default_value=0, 135 | help=""" 136 | CPU usage limit to display to the user. 137 | 138 | Note that this does not actually limit the user's CPU usage! 139 | 140 | Defaults to reading from the `CPU_LIMIT` environment variable. If 141 | set to 0, the total CPU count available is displayed. 142 | """, 143 | ).tag(config=True) 144 | 145 | @default("cpu_limit") 146 | def _cpu_limit_default(self): 147 | return float(os.environ.get("CPU_LIMIT", 0)) 148 | 149 | track_disk_usage = Bool( 150 | default_value=False, 151 | help=""" 152 | Set to True in order to enable reporting of disk usage statistics. 153 | """, 154 | ).tag(config=True) 155 | 156 | @default("disk_path") 157 | def _disk_path_default(self): 158 | return str(os.environ.get("HOME", "/home/jovyan")) 159 | 160 | disk_warning_threshold = Float( 161 | default_value=0.1, 162 | help=""" 163 | Warn user with flashing lights when disk usage is within this fraction 164 | total space. 165 | 166 | For example, if total size is 10G, `disk_warning_threshold` is 0.1, 167 | we will start warning the user when they use (10 - (10 * 0.1)) G. 168 | 169 | Set to 0 to disable warning. 170 | """, 171 | ).tag(config=True) 172 | 173 | enable_prometheus_metrics = Bool( 174 | default_value=True, 175 | help=""" 176 | Set to False in order to disable reporting of Prometheus style metrics. 177 | """, 178 | ).tag(config=True) 179 | 180 | show_host_usage = Bool( 181 | default_value=True, 182 | help=""" 183 | Set to True in order to show host cpu and host virtual memory info. 184 | """, 185 | ).tag(config=True) 186 | -------------------------------------------------------------------------------- /jupyter_resource_usage/static/main.js: -------------------------------------------------------------------------------- 1 | define([ // eslint-disable-line no-undef 2 | 'jquery', 3 | 'base/js/utils' 4 | ], ($, utils) => { 5 | function setupDOM() { 6 | $('#maintoolbar-container').append( 7 | $('
').attr('id', 'jupyter-resource-usage-display-disk') 8 | .addClass('btn-group') 9 | .addClass('jupyter-resource-usage-hide') 10 | .addClass('pull-right').append( 11 | $('').text(' Disk: ') 12 | ).append( 13 | $('').attr('id', 'jupyter-resource-usage-disk') 14 | .attr('title', 'Actively used CPU (updates every 5s)') 15 | ) 16 | ); 17 | $('#maintoolbar-container').append( 18 | $('
').attr('id', 'jupyter-resource-usage-display') 19 | .addClass('btn-group') 20 | .addClass('pull-right') 21 | .append( 22 | $('').text('Memory: ') 23 | ).append( 24 | $('').attr('id', 'jupyter-resource-usage-mem') 25 | .attr('title', 'Actively used Memory (updates every 5s)') 26 | ) 27 | ); 28 | $('#maintoolbar-container').append( 29 | $('
').attr('id', 'jupyter-resource-usage-display-cpu') 30 | .addClass('btn-group') 31 | .addClass('jupyter-resource-usage-hide') 32 | .addClass('pull-right').append( 33 | $('').text(' CPU: ') 34 | ).append( 35 | $('').attr('id', 'jupyter-resource-usage-cpu') 36 | .attr('title', 'Actively used CPU (updates every 5s)') 37 | ) 38 | ); 39 | 40 | $('head').append( 41 | $('