11import os
22import json
33import psutil
4- from traitlets import Float , Int , Union , default
4+ from traitlets import Bool , Float , Int , Union , default
55from traitlets .config import Configurable
66from notebook .utils import url_path_join
77from notebook .base .handlers import IPythonHandler
1515
1616from threading import Thread
1717from concurrent .futures import ThreadPoolExecutor , as_completed
18+ from tornado .concurrent import run_on_executor
1819
1920class MetricsHandler (IPythonHandler ):
2021 def initialize (self ):
2122 super ().initialize ()
2223 self .cpu_percent = 0
24+
25+ # https://www.tornadoweb.org/en/stable/concurrent.html#tornado.concurrent.run_on_executor
26+ self .executor = ThreadPoolExecutor (max_workers = 10 )
27+
2328 # A better approach would use cpu_affinity to account for the
2429 # fact that the number of logical CPUs in the system is not
2530 # necessarily the same as the number of CPUs the process
2631 # can actually use. But cpu_affinity isn't available for OS X.
2732 self .cpu_count = psutil .cpu_count ()
2833
29- def update_cpu_percent ():
30- def get_cpu_percent (p ):
31- try :
32- return p .cpu_percent (interval = 0.1 )
33- # Avoid littering logs with stack traces complaining
34- # about dead processes having no CPU usage
35- except :
36- return 0
37- # This loop should execute roughly every "interval" seconds
38- # Slower if max_workers is much less than the number of processes
39- while True :
40- cur_process = psutil .Process ()
41- all_processes = [cur_process ] + cur_process .children (recursive = True )
42- # Could have a worker for every process
43- with ThreadPoolExecutor (max_workers = 10 ) as executor :
44- cpu_percents = [executor .submit (get_cpu_percent , p ) for p in all_processes ]
45- total_percent = 0
46- for future in as_completed (cpu_percents ):
47- try :
48- total_percent += future .result ()
49- except :
50- pass
51- self .cpu_percent = total_percent
52-
53- t = Thread (target = update_cpu_percent )
54- t .start ()
34+ @run_on_executor
35+ def update_cpu_percent (self , all_processes ):
36+
37+ def get_cpu_percent (p ):
38+ try :
39+ return p .cpu_percent (interval = 0.05 )
40+ # Avoid littering logs with stack traces complaining
41+ # about dead processes having no CPU usage
42+ except :
43+ return 0
44+
45+ return sum ([get_cpu_percent (p ) for p in all_processes ])
5546
5647 @web .authenticated
57- def get (self ):
48+ async def get (self ):
5849 """
5950 Calculate and return current resource usage metrics
6051 """
@@ -68,20 +59,15 @@ def get(self):
6859 else : # mem_limit is an Int
6960 mem_limit = config .mem_limit
7061
71- def get_cpu_percent (p ):
72- try :
73- return p .cpu_percent (interval = 0.1 )
74- # Avoid littering logs with stack traces complaining
75- # about dead processes having no CPU usage
76- except :
77- return 0
78- cpu_percent = sum ([get_cpu_percent (p ) for p in all_processes ])
7962 # A better approach would use cpu_affinity to account for the
8063 # fact that the number of logical CPUs in the system is not
8164 # necessarily the same as the number of CPUs the process
8265 # can actually use. But cpu_affinity isn't available for OS X.
8366 cpu_count = psutil .cpu_count ()
8467
68+ if config .track_cpu_percent :
69+ self .cpu_percent = await self .update_cpu_percent (all_processes )
70+
8571 limits = {}
8672
8773 if config .mem_limit != 0 :
@@ -158,6 +144,13 @@ class ResourceUseDisplay(Configurable):
158144 """
159145 ).tag (config = True )
160146
147+ track_cpu_percent = Bool (
148+ False ,
149+ help = """
150+ Set to True in order to enable reporting of CPU usage statistics.
151+ """
152+ ).tag (config = True )
153+
161154 cpu_warning_threshold = Float (
162155 0.1 ,
163156 help = """
0 commit comments