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
7import logging
8import os
9import subprocess
10import time
11
12from autotest_lib.client.bin import test, utils
13from autotest_lib.client.common_lib import error
14
15class 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