255 lines
8.3 KiB
Python
255 lines
8.3 KiB
Python
#!/usr/bin/python2
|
|
#
|
|
# 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)
|