From 473eee9171e800f5d18eb5d373abd17e2c18fb90 Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Mon, 23 May 2022 16:26:09 +0200 Subject: [PATCH 01/11] ensure pstutil for the process is accurate --- ipykernel/control.py | 25 +++++++++++++++++++++++++ ipykernel/kernelbase.py | 19 ++----------------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/ipykernel/control.py b/ipykernel/control.py index bb5f7b5f9..92e1f9eb6 100644 --- a/ipykernel/control.py +++ b/ipykernel/control.py @@ -1,6 +1,7 @@ from threading import Thread import zmq +import psutil if zmq.pyzmq_version_info() >= (17, 0): from tornado.ioloop import IOLoop @@ -8,13 +9,19 @@ # deprecated since pyzmq 17 from zmq.eventloop.ioloop import IOLoop +import logging +logger = logging.getLogger() +logger.setLevel(logging.INFO) class ControlThread(Thread): + processes = {} + def __init__(self, **kwargs): Thread.__init__(self, name="Control", **kwargs) self.io_loop = IOLoop(make_current=False) self.pydev_do_not_trace = True self.is_pydev_daemon_thread = True + self.cpu_percent = psutil.cpu_percent() def run(self): self.name = "Control" @@ -24,6 +31,24 @@ def run(self): finally: self.io_loop.close() + def get_process_metric_value(self, process, name, attribute=None): + try: + # psutil.Process methods will either return... + pid = process.pid + p = self.processes.get(pid, None) + if not p: + self.processes[process.pid] = process + p = self.processes.get(pid) + metric_value = getattr(p, name)() + if attribute is not None: # ... a named tuple + return getattr(metric_value, attribute) + else: # ... or a number + return metric_value + # Avoid littering logs with stack traces + # complaining about dead processes + except BaseException: + return None + def stop(self): """Stop the thread. diff --git a/ipykernel/kernelbase.py b/ipykernel/kernelbase.py index 59f69bc00..c7007ba6b 100644 --- a/ipykernel/kernelbase.py +++ b/ipykernel/kernelbase.py @@ -953,30 +953,15 @@ async def debug_request(self, stream, ident, parent): reply_msg = self.session.send(stream, "debug_reply", reply_content, parent, ident) self.log.debug("%s", reply_msg) - # Taken from https://github.com/jupyter-server/jupyter-resource-usage/blob/e6ec53fa69fdb6de8e878974bcff006310658408/jupyter_resource_usage/metrics.py#L16 - def get_process_metric_value(self, process, name, attribute=None): - try: - # psutil.Process methods will either return... - metric_value = getattr(process, name)() - if attribute is not None: # ... a named tuple - return getattr(metric_value, attribute) - else: # ... or a number - return metric_value - # Avoid littering logs with stack traces - # complaining about dead processes - except BaseException: - return None - async def usage_request(self, stream, ident, parent): reply_content = {"hostname": socket.gethostname(), "pid": os.getpid()} current_process = psutil.Process() all_processes = [current_process] + current_process.children(recursive=True) - process_metric_value = self.get_process_metric_value reply_content["kernel_cpu"] = sum( - [process_metric_value(process, "cpu_percent", None) for process in all_processes] + [self.control_thread.get_process_metric_value(process, "cpu_percent", None) for process in all_processes] ) reply_content["kernel_memory"] = sum( - [process_metric_value(process, "memory_info", "rss") for process in all_processes] + [self.control_thread.get_process_metric_value(process, "memory_info", "rss") for process in all_processes] ) cpu_percent = psutil.cpu_percent() # https://psutil.readthedocs.io/en/latest/index.html?highlight=cpu#psutil.cpu_percent From 3812b54dd0f7032b2e4e45950c6112017abf5479 Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Tue, 24 May 2022 06:28:07 +0200 Subject: [PATCH 02/11] maintain the process map in kernelbase --- ipykernel/control.py | 20 -------------------- ipykernel/kernelbase.py | 24 ++++++++++++++++++++++-- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/ipykernel/control.py b/ipykernel/control.py index 92e1f9eb6..689306638 100644 --- a/ipykernel/control.py +++ b/ipykernel/control.py @@ -14,14 +14,12 @@ logger.setLevel(logging.INFO) class ControlThread(Thread): - processes = {} def __init__(self, **kwargs): Thread.__init__(self, name="Control", **kwargs) self.io_loop = IOLoop(make_current=False) self.pydev_do_not_trace = True self.is_pydev_daemon_thread = True - self.cpu_percent = psutil.cpu_percent() def run(self): self.name = "Control" @@ -31,24 +29,6 @@ def run(self): finally: self.io_loop.close() - def get_process_metric_value(self, process, name, attribute=None): - try: - # psutil.Process methods will either return... - pid = process.pid - p = self.processes.get(pid, None) - if not p: - self.processes[process.pid] = process - p = self.processes.get(pid) - metric_value = getattr(p, name)() - if attribute is not None: # ... a named tuple - return getattr(metric_value, attribute) - else: # ... or a number - return metric_value - # Avoid littering logs with stack traces - # complaining about dead processes - except BaseException: - return None - def stop(self): """Stop the thread. diff --git a/ipykernel/kernelbase.py b/ipykernel/kernelbase.py index c7007ba6b..e8b2efa4d 100644 --- a/ipykernel/kernelbase.py +++ b/ipykernel/kernelbase.py @@ -76,6 +76,8 @@ class Kernel(SingletonConfigurable): # attribute to override with a GUI eventloop = Any(None) + processes = {} + @observe("eventloop") def _update_eventloop(self, change): """schedule call to eventloop from IOLoop""" @@ -953,15 +955,33 @@ async def debug_request(self, stream, ident, parent): reply_msg = self.session.send(stream, "debug_reply", reply_content, parent, ident) self.log.debug("%s", reply_msg) + def get_process_metric_value(self, process, name, attribute=None): + try: + # psutil.Process methods will either return... + pid = process.pid + p = self.processes.get(pid, None) + if not p: + self.processes[pid] = process + p = self.processes.get(pid) + metric_value = getattr(p, name)() + if attribute is not None: # ... a named tuple + return getattr(metric_value, attribute) + else: # ... or a number + return metric_value + # Avoid littering logs with stack traces + # complaining about dead processes + except BaseException: + return None + async def usage_request(self, stream, ident, parent): reply_content = {"hostname": socket.gethostname(), "pid": os.getpid()} current_process = psutil.Process() all_processes = [current_process] + current_process.children(recursive=True) reply_content["kernel_cpu"] = sum( - [self.control_thread.get_process_metric_value(process, "cpu_percent", None) for process in all_processes] + [self.get_process_metric_value(process, "cpu_percent", None) for process in all_processes] ) reply_content["kernel_memory"] = sum( - [self.control_thread.get_process_metric_value(process, "memory_info", "rss") for process in all_processes] + [self.get_process_metric_value(process, "memory_info", "rss") for process in all_processes] ) cpu_percent = psutil.cpu_percent() # https://psutil.readthedocs.io/en/latest/index.html?highlight=cpu#psutil.cpu_percent From add40d25bf2641dd4960f97d6f876bea98930191 Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Tue, 24 May 2022 06:29:06 +0200 Subject: [PATCH 03/11] remove unneeded imports --- ipykernel/control.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/ipykernel/control.py b/ipykernel/control.py index 689306638..c1de9d15d 100644 --- a/ipykernel/control.py +++ b/ipykernel/control.py @@ -1,7 +1,6 @@ from threading import Thread import zmq -import psutil if zmq.pyzmq_version_info() >= (17, 0): from tornado.ioloop import IOLoop @@ -9,10 +8,6 @@ # deprecated since pyzmq 17 from zmq.eventloop.ioloop import IOLoop -import logging -logger = logging.getLogger() -logger.setLevel(logging.INFO) - class ControlThread(Thread): def __init__(self, **kwargs): From 676b1b1bb6b0aa928854f4c6788d9a722de18a44 Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Tue, 24 May 2022 06:29:49 +0200 Subject: [PATCH 04/11] remove unneeded imports --- ipykernel/control.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ipykernel/control.py b/ipykernel/control.py index c1de9d15d..fe1645284 100644 --- a/ipykernel/control.py +++ b/ipykernel/control.py @@ -8,6 +8,7 @@ # deprecated since pyzmq 17 from zmq.eventloop.ioloop import IOLoop + class ControlThread(Thread): def __init__(self, **kwargs): From d08c94a95e2cffa64e76f31b7b8e5b5d687aa7d7 Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Tue, 24 May 2022 06:30:08 +0200 Subject: [PATCH 05/11] remove unneeded imports --- ipykernel/control.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ipykernel/control.py b/ipykernel/control.py index fe1645284..bb5f7b5f9 100644 --- a/ipykernel/control.py +++ b/ipykernel/control.py @@ -10,7 +10,6 @@ class ControlThread(Thread): - def __init__(self, **kwargs): Thread.__init__(self, name="Control", **kwargs) self.io_loop = IOLoop(make_current=False) From d82ad7203e066e4f63095796a40f6f8efe63f57b Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Mon, 6 Jun 2022 15:19:57 +0200 Subject: [PATCH 06/11] prune the unexisting processes --- ipykernel/kernelbase.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ipykernel/kernelbase.py b/ipykernel/kernelbase.py index e8b2efa4d..07672ee0a 100644 --- a/ipykernel/kernelbase.py +++ b/ipykernel/kernelbase.py @@ -977,6 +977,12 @@ async def usage_request(self, stream, ident, parent): reply_content = {"hostname": socket.gethostname(), "pid": os.getpid()} current_process = psutil.Process() all_processes = [current_process] + current_process.children(recursive=True) + pruned_processes = {} + for process in all_processes: + p = self.processes.get(process.pid) + if p is not None: + pruned_processes[process.pid] = p + self.processes = pruned_processes reply_content["kernel_cpu"] = sum( [self.get_process_metric_value(process, "cpu_percent", None) for process in all_processes] ) From a7b2b27645b3d26808545488725590d10452992a Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Wed, 8 Jun 2022 06:18:14 +0200 Subject: [PATCH 07/11] do not creazte intermediate structure to prune the proceses --- ipykernel/kernelbase.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ipykernel/kernelbase.py b/ipykernel/kernelbase.py index 07672ee0a..303f0a7af 100644 --- a/ipykernel/kernelbase.py +++ b/ipykernel/kernelbase.py @@ -977,12 +977,12 @@ async def usage_request(self, stream, ident, parent): reply_content = {"hostname": socket.gethostname(), "pid": os.getpid()} current_process = psutil.Process() all_processes = [current_process] + current_process.children(recursive=True) - pruned_processes = {} - for process in all_processes: - p = self.processes.get(process.pid) - if p is not None: - pruned_processes[process.pid] = p - self.processes = pruned_processes + # 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) for process in all_processes} + self.processes = { + process.pid: self.processes.get(process.pid, process) for process in all_processes + } reply_content["kernel_cpu"] = sum( [self.get_process_metric_value(process, "cpu_percent", None) for process in all_processes] ) From 2b28ce81e2ade1efb78bf32dfac6b31957bb9e02 Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Wed, 8 Jun 2022 15:52:13 +0200 Subject: [PATCH 08/11] correctly maange the process map --- ipykernel/kernelbase.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/ipykernel/kernelbase.py b/ipykernel/kernelbase.py index 303f0a7af..872f8111d 100644 --- a/ipykernel/kernelbase.py +++ b/ipykernel/kernelbase.py @@ -957,13 +957,7 @@ async def debug_request(self, stream, ident, parent): def get_process_metric_value(self, process, name, attribute=None): try: - # psutil.Process methods will either return... - pid = process.pid - p = self.processes.get(pid, None) - if not p: - self.processes[pid] = process - p = self.processes.get(pid) - metric_value = getattr(p, name)() + metric_value = getattr(process, name)() if attribute is not None: # ... a named tuple return getattr(metric_value, attribute) else: # ... or a number @@ -979,15 +973,14 @@ async def usage_request(self, stream, ident, parent): 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) for process in all_processes} self.processes = { process.pid: self.processes.get(process.pid, process) for process in all_processes } reply_content["kernel_cpu"] = sum( - [self.get_process_metric_value(process, "cpu_percent", None) for process in all_processes] + [self.get_process_metric_value(process, "cpu_percent", None) for process in self.processes.values()] ) reply_content["kernel_memory"] = sum( - [self.get_process_metric_value(process, "memory_info", "rss") for process in all_processes] + [self.get_process_metric_value(process, "memory_info", "rss") for process in self.processes.values()] ) cpu_percent = psutil.cpu_percent() # https://psutil.readthedocs.io/en/latest/index.html?highlight=cpu#psutil.cpu_percent From 3a9d9de7907fbf0495c3a08e35714e2980a620a2 Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Thu, 9 Jun 2022 04:59:56 +0200 Subject: [PATCH 09/11] lint --- ipykernel/kernelbase.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/ipykernel/kernelbase.py b/ipykernel/kernelbase.py index 872f8111d..cebaa41f5 100644 --- a/ipykernel/kernelbase.py +++ b/ipykernel/kernelbase.py @@ -977,10 +977,16 @@ async def usage_request(self, stream, ident, parent): process.pid: self.processes.get(process.pid, process) for process in all_processes } reply_content["kernel_cpu"] = sum( - [self.get_process_metric_value(process, "cpu_percent", None) for process in self.processes.values()] + [ + self.get_process_metric_value(process, "cpu_percent", None) + for process in self.processes.values() + ] ) reply_content["kernel_memory"] = sum( - [self.get_process_metric_value(process, "memory_info", "rss") for process in self.processes.values()] + [ + self.get_process_metric_value(process, "memory_info", "rss") + for process in self.processes.values() + ] ) cpu_percent = psutil.cpu_percent() # https://psutil.readthedocs.io/en/latest/index.html?highlight=cpu#psutil.cpu_percent From 3ceabee3da357769d190b31d1652ad2e369ac6e3 Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Thu, 9 Jun 2022 05:08:45 +0200 Subject: [PATCH 10/11] var annotation for the processes class field --- ipykernel/kernelbase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ipykernel/kernelbase.py b/ipykernel/kernelbase.py index cebaa41f5..4c9fee52a 100644 --- a/ipykernel/kernelbase.py +++ b/ipykernel/kernelbase.py @@ -76,7 +76,7 @@ class Kernel(SingletonConfigurable): # attribute to override with a GUI eventloop = Any(None) - processes = {} + processes: dict[str, psutil.Process] = {} @observe("eventloop") def _update_eventloop(self, change): From 361bd90b8381a7aecc2b998ac04880a2a612fff9 Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Thu, 9 Jun 2022 16:37:35 +0200 Subject: [PATCH 11/11] use typing package for python 3.8 --- ipykernel/kernelbase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ipykernel/kernelbase.py b/ipykernel/kernelbase.py index 4c9fee52a..30ab173f6 100644 --- a/ipykernel/kernelbase.py +++ b/ipykernel/kernelbase.py @@ -76,7 +76,7 @@ class Kernel(SingletonConfigurable): # attribute to override with a GUI eventloop = Any(None) - processes: dict[str, psutil.Process] = {} + processes: t.Dict[str, psutil.Process] = {} @observe("eventloop") def _update_eventloop(self, change):