Home | History | Annotate | Download | only in kernel_SchedBandwith
      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