普通文本  |  255行  |  8.28 KB

#!/usr/bin/python
#
# Copyright (c) 2012 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import logging
import os
import subprocess
import time

from autotest_lib.client.bin import test, utils
from autotest_lib.client.common_lib import error

class kernel_SchedBandwith(test.test):
    """Test kernel CFS_BANDWIDTH scheduler mechanism (/sys/fs/cgroup/...)"""
    version = 1
    # A 30 second (default) run should result in most of the time slices being
    # throttled.  Set a conservative lower bound based on having an unknown
    # system load.  Alex commonly yields numbers in the range 311..315, which
    # includes test overhead and signal latency.
    _MIN_SECS = 30

    _CG_DIR = "/sys/fs/cgroup/cpu"
    _CG_CRB_DIR = os.path.join(_CG_DIR, "chrome_renderers", "background")

    def _parse_cpu_stats(self):
        """Parse and return CFS bandwidth statistics.

        From kernel/Documentation/scheduler/sched-bwc.txt

        cpu.stat:
        - nr_periods: Number of enforcement intervals that have elapsed.
        - nr_throttled: Number of times the group has been throttled/limited.
        - throttled_time: The total time duration (in nanoseconds) for which entities
          of the group have been throttled.

        Returns: tuple with nr_periods, nr_throttled, throttled_time.
        """
        nr_periods = None
        nr_throttled = None
        throttled_time = None

        fd = open(os.path.join(self._CG_CRB_DIR, "cpu.stat"))

        for ln in fd.readlines():
            logging.debug(ln)
            (name, val) = ln.split()
            logging.debug("name = %s val = %s", name, val)
            if name == 'nr_periods':
                nr_periods = int(val)
            if name == 'nr_throttled':
                nr_throttled = int(val)
            if name == 'throttled_time':
                throttled_time = int(val)

        fd.close()
        return nr_periods, nr_throttled, throttled_time

    @staticmethod
    def _parse_pid_stats(pid):
        """Parse process id stats to determin CPU utilization.

           from: https://www.kernel.org/doc/Documentation/scheduler/sched-stats.txt

           /proc/<pid>/schedstat
           ----------------
           schedstats also adds a new /proc/<pid>/schedstat file to include some
           of the same information on a per-process level.  There are three
           fields in this file correlating for that process to:
                1) time spent on the cpu
                2) time spent waiting on a runqueue
                3) # of timeslices run on this cpu

        Args:
            pid: integer, process id to gather stats for.

        Returns:
            tuple with total_msecs and idle_msecs
        """
        idle_slices = 0
        total_slices = 0

        fname = "/proc/sys/kernel/sched_cfs_bandwidth_slice_us"
        timeslice_ms = int(utils.read_one_line(fname).strip()) / 1000.

        with open(os.path.join('/proc', str(pid), 'schedstat')) as fd:
            values = list(int(val) for val in fd.readline().strip().split())
            running_slices = values[0] / timeslice_ms
            idle_slices = values[1] / timeslice_ms
            total_slices = running_slices + idle_slices
        return (total_slices, idle_slices)


    def _cg_start_task(self, in_cgroup=True):
        """Start a CPU hogging task and add to cgroup.

        Args:
            in_cgroup: Boolean, if true add to cgroup otherwise just start.

        Returns:
            integer of pid of task started
        """
        null_fd = open("/dev/null", "w")
        cmd = ['seq', '0', '0', '0']
        task = subprocess.Popen(cmd, stdout=null_fd)
        self._tasks.append(task)

        if in_cgroup:
            utils.write_one_line(os.path.join(self._CG_CRB_DIR, "tasks"),
                                 task.pid)
        return task.pid


    def _cg_stop_tasks(self):
        """Stop CPU hogging task."""
        if hasattr(self, '_tasks') and self._tasks:
            for task in self._tasks:
                task.kill()
        self._tasks = []


    def _cg_set_quota(self, quota=-1):
        """Set CPU quota that can be used for cgroup

        Default of -1 will disable throttling
        """
        utils.write_one_line(os.path.join(self._CG_CRB_DIR, "cpu.cfs_quota_us"),
                             quota)
        rd_quota = utils.read_one_line(os.path.join(self._CG_CRB_DIR,
                                                    "cpu.cfs_quota_us"))
        if rd_quota != quota:
            error.TestFail("Setting cpu quota to %d" % quota)


    def _cg_total_shares(self):
        if not hasattr(self, '_total_shares'):
            self._total_shares = int(utils.read_one_line(
                    os.path.join(self._CG_DIR, "cpu.shares")))
        return self._total_shares


    def _cg_set_shares(self, shares=None):
        """Set CPU shares that can be used for cgroup

        Default of None reads total shares for cpu group and assigns that so
        there will be no throttling
        """
        if shares is None:
            shares = self._cg_total_shares()
        utils.write_one_line(os.path.join(self._CG_CRB_DIR, "cpu.shares"),
                             shares)
        rd_shares = utils.read_one_line(os.path.join(self._CG_CRB_DIR,
                                                  "cpu.shares"))
        if rd_shares != shares:
            error.TestFail("Setting cpu shares to %d" % shares)


    def _cg_disable_throttling(self):
        self._cg_set_quota()
        self._cg_set_shares()


    def _cg_test_quota(self):
        stats = []
        period_us = int(utils.read_one_line(os.path.join(self._CG_CRB_DIR,
                                                     "cpu.cfs_period_us")))

        stats.append(self._parse_cpu_stats())

        self._cg_start_task()
        self._cg_set_quota(int(period_us * 0.1))
        time.sleep(self._MIN_SECS)

        stats.append(self._parse_cpu_stats())

        self._cg_stop_tasks()
        return stats


    def _cg_test_shares(self):
        stats = []

        self._cg_set_shares(2)
        pid = self._cg_start_task()
        stats.append(self._parse_pid_stats(pid))

        # load system heavily
        for _ in xrange(utils.count_cpus() * 2 + 1):
            self._cg_start_task(in_cgroup=False)

        time.sleep(self._MIN_SECS)

        stats.append(self._parse_pid_stats(pid))

        self._cg_stop_tasks()
        return stats


    @staticmethod
    def _check_stats(name, stats, percent):
        total = stats[1][0] - stats[0][0]
        idle = stats[1][1] - stats[0][1]
        logging.info("%s total:%d idle:%d",
                     name, total, idle)

        # make sure we idled at least X% of the slices
        min_idle = int(percent * total)
        if idle < min_idle:
            logging.error("%s idle count %d < %d ", name, idle,
                          min_idle)
            return 1
        return 0


    def setup(self):
        super(kernel_SchedBandwith, self).setup()
        self._tasks = []
        self._quota = None
        self._shares = None


    def run_once(self, test_quota=True, test_shares=True):
        errors = 0
        if not os.path.exists(self._CG_CRB_DIR):
            raise error.TestError("Locating cgroup dir %s" % self._CG_CRB_DIR)

        self._quota = utils.read_one_line(os.path.join(self._CG_CRB_DIR,
                                                       "cpu.cfs_quota_us"))
        self._shares = utils.read_one_line(os.path.join(self._CG_CRB_DIR,
                                                        "cpu.shares"))
        if test_quota:
            self._cg_disable_throttling()
            quota_stats = self._cg_test_quota()
            errors += self._check_stats('quota', quota_stats, 0.9)

        if test_shares:
            self._cg_disable_throttling()
            shares_stats = self._cg_test_shares()
            errors += self._check_stats('shares', shares_stats, 0.6)

        if errors:
            error.TestFail("Cgroup bandwidth throttling not working")


    def cleanup(self):
        super(kernel_SchedBandwith, self).cleanup()
        self._cg_stop_tasks()

        if hasattr(self, '_quota') and self._quota is not None:
            self._cg_set_quota(self._quota)

        if hasattr(self, '_shares') and self._shares is not None:
            self._cg_set_shares(self._shares)