Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 26 additions & 6 deletions ipykernel/kernelbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
# jupyter_client < 5, use local now()
now = datetime.now

import psutil
import zmq
from IPython.core.error import StdinNotImplementedError
from jupyter_client.session import Session
Expand All @@ -62,6 +61,19 @@
from .iostream import OutStream
from .utils import LazyDict, _async_in_context

psutil: t.Any | None
try:
import psutil as _psutil
except ImportError:
psutil = None
else:
psutil = _psutil

if psutil is None:
_NO_SUCH_PROCESS: tuple[type[BaseException], ...] = ()
else:
_NO_SUCH_PROCESS = (psutil.NoSuchProcess,)

_AWAITABLE_MESSAGE: str = (
"For consistency across implementations, it is recommended that `{func_name}`"
" either be a coroutine function (`async def`) or return an awaitable object"
Expand Down Expand Up @@ -97,7 +109,7 @@ class Kernel(SingletonConfigurable):
# attribute to override with a GUI
eventloop = Any(None)

processes: dict[str, psutil.Process] = {}
processes: dict[int, t.Any] = {}

@observe("eventloop")
def _update_eventloop(self, change):
Expand Down Expand Up @@ -1172,13 +1184,18 @@ async def usage_request(self, stream, ident, parent):
if not self.session:
return
reply_content = {"hostname": socket.gethostname(), "pid": os.getpid()}
if psutil is None:
reply_content["cpu_count"] = os.cpu_count()
reply_msg = self.session.send(stream, "usage_reply", reply_content, parent, ident)
self.log.debug("%s", reply_msg)
return

current_process = psutil.Process()
all_processes = [current_process, *current_process.children(recursive=True)]
# Ensure 1) self.processes is updated to only current subprocesses
# and 2) we reuse processes when possible (needed for accurate CPU)
self.processes = {
process.pid: self.processes.get(process.pid, process) # type:ignore[misc,call-overload]
for process in all_processes
process.pid: self.processes.get(process.pid, process) for process in all_processes
}
reply_content["kernel_cpu"] = sum(
[
Expand All @@ -1196,7 +1213,7 @@ async def usage_request(self, stream, ident, parent):
cpu_percent = psutil.cpu_percent()
# https://psutil.readthedocs.io/en/latest/index.html?highlight=cpu#psutil.cpu_percent
# The first time cpu_percent is called it will return a meaningless 0.0 value which you are supposed to ignore.
if cpu_percent is not None and cpu_percent != 0.0: # type:ignore[redundant-expr]
if cpu_percent is not None and cpu_percent != 0.0:
reply_content["host_cpu_percent"] = cpu_percent
reply_content["cpu_count"] = psutil.cpu_count(logical=True)
reply_content["host_virtual_memory"] = dict(psutil.virtual_memory()._asdict())
Expand Down Expand Up @@ -1476,7 +1493,7 @@ def _signal_children(self, signum):
p.kill()
else:
p.send_signal(signum)
except psutil.NoSuchProcess:
except _NO_SUCH_PROCESS:
pass

def _process_children(self):
Expand All @@ -1486,6 +1503,9 @@ def _process_children(self):
- including parents and self with killpg
- including all children that may have forked-off a new group
"""
if psutil is None:
return []

kernel_process = psutil.Process()
all_children = kernel_process.children(recursive=True)
if os.name == "nt":
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ dependencies = [
"matplotlib-inline>=0.1",
'appnope>=0.1.2;platform_system=="Darwin"',
"pyzmq>=25",
"psutil>=5.7",
"packaging>=22",
]

Expand Down Expand Up @@ -59,6 +58,7 @@ test = [
"flaky",
"ipyparallel",
"pre-commit",
"psutil>=5.7",
"pytest-asyncio>=0.23.5",
"pytest-timeout"
]
Expand Down
27 changes: 27 additions & 0 deletions tests/test_kernel_direct.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# Distributed under the terms of the Modified BSD License.

import os
import signal
import warnings

import pytest
Expand Down Expand Up @@ -152,6 +153,32 @@ async def test_connect_request(kernel):
await kernel.connect_request(kernel.shell_stream, "foo", {})


async def test_usage_request_without_psutil(kernel, monkeypatch):
import ipykernel.kernelbase as kernelbase

monkeypatch.setattr(kernelbase, "psutil", None)
reply = await kernel.test_control_message("usage_request", {})
content = reply["content"]

assert reply["header"]["msg_type"] == "usage_reply"
assert content["hostname"]
assert content["pid"] == os.getpid()
assert content["cpu_count"] == os.cpu_count()
assert "host_cpu_percent" not in content
assert "host_virtual_memory" not in content
assert "kernel_memory" not in content


async def test_child_process_fallbacks_without_psutil(kernel, monkeypatch):
import ipykernel.kernelbase as kernelbase

monkeypatch.setattr(kernelbase, "psutil", None)

assert kernel._process_children() == []
kernel._signal_children(signal.SIGTERM)
await kernel._progressively_terminate_all_children()


async def test_send_interrupt_children(kernel):
kernel._send_interrupt_children()

Expand Down
Loading