Skip to content

Commit 9b25fbb

Browse files
authored
Track CPU usage (#21)
Track CPU usage
2 parents 9180775 + 17cdf4c commit 9b25fbb

File tree

2 files changed

+95
-8
lines changed

2 files changed

+95
-8
lines changed

nbresuse/__init__.py

Lines changed: 95 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,98 @@
11
import os
22
import json
33
import psutil
4-
from traitlets import Float, Int, Union, default
4+
from traitlets import Bool, Float, Int, Union, default
55
from traitlets.config import Configurable
66
from notebook.utils import url_path_join
77
from notebook.base.handlers import IPythonHandler
88
from tornado import web
9+
910
try:
1011
# Traitlets >= 4.3.3
1112
from traitlets import Callable
1213
except 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

1619
class 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+
102189
def load_jupyter_server_extension(nbapp):
103190
"""
104191
Called during notebook start
File renamed without changes.

0 commit comments

Comments
 (0)