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
88from tornado import web
9+
910try :
1011 # Traitlets >= 4.3.3
1112 from traitlets import Callable
1213except ImportError :
13- from .callable import Callable
14+ from .utils import Callable
1415
16+ from concurrent .futures import ThreadPoolExecutor
17+ from tornado .concurrent import run_on_executor
1518
1619class MetricsHandler (IPythonHandler ):
20+ def initialize (self ):
21+ super ().initialize ()
22+ self .cpu_percent = 0
23+
24+ # https://www.tornadoweb.org/en/stable/concurrent.html#tornado.concurrent.run_on_executor
25+ self .executor = ThreadPoolExecutor (max_workers = 10 )
26+
27+ self .cpu_count = psutil .cpu_count ()
28+
29+ @run_on_executor
30+ def update_cpu_percent (self , all_processes ):
31+
32+ def get_cpu_percent (p ):
33+ try :
34+ return p .cpu_percent (interval = 0.05 )
35+ # Avoid littering logs with stack traces complaining
36+ # about dead processes having no CPU usage
37+ except :
38+ return 0
39+
40+ return sum ([get_cpu_percent (p ) for p in all_processes ])
41+
1742 @web .authenticated
18- def get (self ):
43+ async def get (self ):
1944 """
2045 Calculate and return current resource usage metrics
2146 """
2247 config = self .settings ['nbresuse_display_config' ]
2348 cur_process = psutil .Process ()
24- all_processes = [cur_process ] + cur_process .children (recursive = True )
49+ all_processes = [cur_process ] + cur_process .children (recursive = True )
50+ limits = {}
51+
52+ # Get memory information
2553 rss = sum ([p .memory_info ().rss for p in all_processes ])
2654
2755 if callable (config .mem_limit ):
2856 mem_limit = config .mem_limit (rss = rss )
2957 else : # mem_limit is an Int
3058 mem_limit = config .mem_limit
3159
32- limits = {}
60+ # A better approach would use cpu_affinity to account for the
61+ # fact that the number of logical CPUs in the system is not
62+ # necessarily the same as the number of CPUs the process
63+ # can actually use. But cpu_affinity isn't available for OS X.
64+ cpu_count = psutil .cpu_count ()
65+
66+ if config .track_cpu_percent :
67+ self .cpu_percent = await self .update_cpu_percent (all_processes )
3368
3469 if config .mem_limit != 0 :
3570 limits ['memory' ] = {
3671 'rss' : mem_limit
3772 }
3873 if config .mem_warning_threshold != 0 :
39- limits ['memory' ]['warn' ] = (config .mem_limit - rss ) < (config .mem_limit * config .mem_warning_threshold )
74+ limits ['memory' ]['warn' ] = (mem_limit - rss ) < (mem_limit * config .mem_warning_threshold )
75+
76+ # Optionally get CPU information
77+ if config .track_cpu_percent :
78+ self .cpu_percent = await self .update_cpu_percent (all_processes )
79+
80+ if config .cpu_limit != 0 :
81+ limits ['cpu' ] = {
82+ 'cpu' : config .cpu_limit
83+ }
84+ if config .cpu_warning_threshold != 0 :
85+ limits ['cpu' ]['warn' ] = (config .cpu_limit - self .cpu_percent ) < (config .cpu_limit * config .cpu_warning_threshold )
86+
4087 metrics = {
4188 'rss' : rss ,
4289 'limits' : limits ,
4390 }
91+ if config .track_cpu_percent :
92+ metrics .update (cpu_percent = self .cpu_percent ,
93+ cpu_count = self .cpu_count )
94+
95+ self .log .debug ("NBResuse metrics: %s" , metrics )
4496 self .write (json .dumps (metrics ))
4597
4698
@@ -69,7 +121,7 @@ class ResourceUseDisplay(Configurable):
69121 """
70122
71123 mem_warning_threshold = Float (
72- 0.1 ,
124+ default_value = 0.1 ,
73125 help = """
74126 Warn user with flashing lights when memory usage is within this fraction
75127 memory limit.
@@ -83,7 +135,6 @@ class ResourceUseDisplay(Configurable):
83135
84136 mem_limit = Union (
85137 trait_types = [Int (), Callable ()],
86- 0 ,
87138 help = """
88139 Memory limit to display to the user, in bytes.
89140 Can also be a function which calculates the memory limit.
@@ -99,6 +150,42 @@ class ResourceUseDisplay(Configurable):
99150 def _mem_limit_default (self ):
100151 return int (os .environ .get ('MEM_LIMIT' , 0 ))
101152
153+ track_cpu_percent = Bool (
154+ default_value = False ,
155+ help = """
156+ Set to True in order to enable reporting of CPU usage statistics.
157+ """
158+ ).tag (config = True )
159+
160+ cpu_warning_threshold = Float (
161+ default_value = 0.1 ,
162+ help = """
163+ Warn user with flashing lights when CPU usage is within this fraction
164+ CPU usage limit.
165+
166+ For example, if CPU limit is 150%, `cpu_warning_threshold` is 0.1,
167+ we will start warning the user when they use (150 - (150 * 0.1)) %.
168+
169+ Set to 0 to disable warning.
170+ """
171+ ).tag (config = True )
172+
173+ cpu_limit = Float (
174+ default_value = 0 ,
175+ help = """
176+ CPU usage limit to display to the user.
177+
178+ Note that this does not actually limit the user's CPU usage!
179+
180+ Defaults to reading from the `CPU_LIMIT` environment variable. If
181+ set to 0, no CPU usage limit is displayed.
182+ """
183+ ).tag (config = True )
184+
185+ @default ('cpu_limit' )
186+ def _cpu_limit_default (self ):
187+ return float (os .environ .get ('CPU_LIMIT' , 0 ))
188+
102189def load_jupyter_server_extension (nbapp ):
103190 """
104191 Called during notebook start
0 commit comments