1 #!/usr/bin/python 2 # 3 # Copyright (c) 2012 The Chromium Authors. All rights reserved. 4 # Use of this source code is governed by a BSD-style license that can be 5 # found in the LICENSE file. 6 7 import logging 8 import os 9 import subprocess 10 import time 11 12 from autotest_lib.client.bin import test, utils 13 from autotest_lib.client.common_lib import error 14 15 class kernel_SchedBandwith(test.test): 16 """Test kernel CFS_BANDWIDTH scheduler mechanism (/sys/fs/cgroup/...)""" 17 version = 1 18 # A 30 second (default) run should result in most of the time slices being 19 # throttled. Set a conservative lower bound based on having an unknown 20 # system load. Alex commonly yields numbers in the range 311..315, which 21 # includes test overhead and signal latency. 22 _MIN_SECS = 30 23 24 _CG_DIR = "/sys/fs/cgroup/cpu" 25 _CG_CRB_DIR = os.path.join(_CG_DIR, "chrome_renderers", "background") 26 27 def _parse_cpu_stats(self): 28 """Parse and return CFS bandwidth statistics. 29 30 From kernel/Documentation/scheduler/sched-bwc.txt 31 32 cpu.stat: 33 - nr_periods: Number of enforcement intervals that have elapsed. 34 - nr_throttled: Number of times the group has been throttled/limited. 35 - throttled_time: The total time duration (in nanoseconds) for which entities 36 of the group have been throttled. 37 38 Returns: tuple with nr_periods, nr_throttled, throttled_time. 39 """ 40 nr_periods = None 41 nr_throttled = None 42 throttled_time = None 43 44 fd = open(os.path.join(self._CG_CRB_DIR, "cpu.stat")) 45 46 for ln in fd.readlines(): 47 logging.debug(ln) 48 (name, val) = ln.split() 49 logging.debug("name = %s val = %s", name, val) 50 if name == 'nr_periods': 51 nr_periods = int(val) 52 if name == 'nr_throttled': 53 nr_throttled = int(val) 54 if name == 'throttled_time': 55 throttled_time = int(val) 56 57 fd.close() 58 return nr_periods, nr_throttled, throttled_time 59 60 @staticmethod 61 def _parse_pid_stats(pid): 62 """Parse process id stats to determin CPU utilization. 63 64 from: https://www.kernel.org/doc/Documentation/scheduler/sched-stats.txt 65 66 /proc/<pid>/schedstat 67 ---------------- 68 schedstats also adds a new /proc/<pid>/schedstat file to include some 69 of the same information on a per-process level. There are three 70 fields in this file correlating for that process to: 71 1) time spent on the cpu 72 2) time spent waiting on a runqueue 73 3) # of timeslices run on this cpu 74 75 Args: 76 pid: integer, process id to gather stats for. 77 78 Returns: 79 tuple with total_msecs and idle_msecs 80 """ 81 idle_slices = 0 82 total_slices = 0 83 84 fname = "/proc/sys/kernel/sched_cfs_bandwidth_slice_us" 85 timeslice_ms = int(utils.read_one_line(fname).strip()) / 1000. 86 87 with open(os.path.join('/proc', str(pid), 'schedstat')) as fd: 88 values = list(int(val) for val in fd.readline().strip().split()) 89 running_slices = values[0] / timeslice_ms 90 idle_slices = values[1] / timeslice_ms 91 total_slices = running_slices + idle_slices 92 return (total_slices, idle_slices) 93 94 95 def _cg_start_task(self, in_cgroup=True): 96 """Start a CPU hogging task and add to cgroup. 97 98 Args: 99 in_cgroup: Boolean, if true add to cgroup otherwise just start. 100 101 Returns: 102 integer of pid of task started 103 """ 104 null_fd = open("/dev/null", "w") 105 cmd = ['seq', '0', '0', '0'] 106 task = subprocess.Popen(cmd, stdout=null_fd) 107 self._tasks.append(task) 108 109 if in_cgroup: 110 utils.write_one_line(os.path.join(self._CG_CRB_DIR, "tasks"), 111 task.pid) 112 return task.pid 113 114 115 def _cg_stop_tasks(self): 116 """Stop CPU hogging task.""" 117 if hasattr(self, '_tasks') and self._tasks: 118 for task in self._tasks: 119 task.kill() 120 self._tasks = [] 121 122 123 def _cg_set_quota(self, quota=-1): 124 """Set CPU quota that can be used for cgroup 125 126 Default of -1 will disable throttling 127 """ 128 utils.write_one_line(os.path.join(self._CG_CRB_DIR, "cpu.cfs_quota_us"), 129 quota) 130 rd_quota = utils.read_one_line(os.path.join(self._CG_CRB_DIR, 131 "cpu.cfs_quota_us")) 132 if rd_quota != quota: 133 error.TestFail("Setting cpu quota to %d" % quota) 134 135 136 def _cg_total_shares(self): 137 if not hasattr(self, '_total_shares'): 138 self._total_shares = int(utils.read_one_line( 139 os.path.join(self._CG_DIR, "cpu.shares"))) 140 return self._total_shares 141 142 143 def _cg_set_shares(self, shares=None): 144 """Set CPU shares that can be used for cgroup 145 146 Default of None reads total shares for cpu group and assigns that so 147 there will be no throttling 148 """ 149 if shares is None: 150 shares = self._cg_total_shares() 151 utils.write_one_line(os.path.join(self._CG_CRB_DIR, "cpu.shares"), 152 shares) 153 rd_shares = utils.read_one_line(os.path.join(self._CG_CRB_DIR, 154 "cpu.shares")) 155 if rd_shares != shares: 156 error.TestFail("Setting cpu shares to %d" % shares) 157 158 159 def _cg_disable_throttling(self): 160 self._cg_set_quota() 161 self._cg_set_shares() 162 163 164 def _cg_test_quota(self): 165 stats = [] 166 period_us = int(utils.read_one_line(os.path.join(self._CG_CRB_DIR, 167 "cpu.cfs_period_us"))) 168 169 stats.append(self._parse_cpu_stats()) 170 171 self._cg_start_task() 172 self._cg_set_quota(int(period_us * 0.1)) 173 time.sleep(self._MIN_SECS) 174 175 stats.append(self._parse_cpu_stats()) 176 177 self._cg_stop_tasks() 178 return stats 179 180 181 def _cg_test_shares(self): 182 stats = [] 183 184 self._cg_set_shares(2) 185 pid = self._cg_start_task() 186 stats.append(self._parse_pid_stats(pid)) 187 188 # load system heavily 189 for _ in xrange(utils.count_cpus() * 2 + 1): 190 self._cg_start_task(in_cgroup=False) 191 192 time.sleep(self._MIN_SECS) 193 194 stats.append(self._parse_pid_stats(pid)) 195 196 self._cg_stop_tasks() 197 return stats 198 199 200 @staticmethod 201 def _check_stats(name, stats, percent): 202 total = stats[1][0] - stats[0][0] 203 idle = stats[1][1] - stats[0][1] 204 logging.info("%s total:%d idle:%d", 205 name, total, idle) 206 207 # make sure we idled at least X% of the slices 208 min_idle = int(percent * total) 209 if idle < min_idle: 210 logging.error("%s idle count %d < %d ", name, idle, 211 min_idle) 212 return 1 213 return 0 214 215 216 def setup(self): 217 super(kernel_SchedBandwith, self).setup() 218 self._tasks = [] 219 self._quota = None 220 self._shares = None 221 222 223 def run_once(self, test_quota=True, test_shares=True): 224 errors = 0 225 if not os.path.exists(self._CG_CRB_DIR): 226 raise error.TestError("Locating cgroup dir %s" % self._CG_CRB_DIR) 227 228 self._quota = utils.read_one_line(os.path.join(self._CG_CRB_DIR, 229 "cpu.cfs_quota_us")) 230 self._shares = utils.read_one_line(os.path.join(self._CG_CRB_DIR, 231 "cpu.shares")) 232 if test_quota: 233 self._cg_disable_throttling() 234 quota_stats = self._cg_test_quota() 235 errors += self._check_stats('quota', quota_stats, 0.9) 236 237 if test_shares: 238 self._cg_disable_throttling() 239 shares_stats = self._cg_test_shares() 240 errors += self._check_stats('shares', shares_stats, 0.6) 241 242 if errors: 243 error.TestFail("Cgroup bandwidth throttling not working") 244 245 246 def cleanup(self): 247 super(kernel_SchedBandwith, self).cleanup() 248 self._cg_stop_tasks() 249 250 if hasattr(self, '_quota') and self._quota is not None: 251 self._cg_set_quota(self._quota) 252 253 if hasattr(self, '_shares') and self._shares is not None: 254 self._cg_set_shares(self._shares) 255