From f7c2a092cbadf9e4c4dc1d4b02e259a646f13471 Mon Sep 17 00:00:00 2001 From: Brendan Jackman Date: Wed, 10 May 2017 11:22:26 +0100 Subject: [PATCH] instrument/schedstats: Add initial instrument for schedstats --- devlib/__init__.py | 1 + devlib/instrument/schedstats.py | 204 ++++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 devlib/instrument/schedstats.py diff --git a/devlib/__init__.py b/devlib/__init__.py index 51a8e471d..b662da1f5 100644 --- a/devlib/__init__.py +++ b/devlib/__init__.py @@ -16,6 +16,7 @@ from devlib.instrument.hwmon import HwmonInstrument from devlib.instrument.monsoon import MonsoonInstrument from devlib.instrument.netstats import NetstatsInstrument +from devlib.instrument.schedstats import SchedstatsInstrument from devlib.trace.ftrace import FtraceCollector diff --git a/devlib/instrument/schedstats.py b/devlib/instrument/schedstats.py new file mode 100644 index 000000000..3e16d4fa2 --- /dev/null +++ b/devlib/instrument/schedstats.py @@ -0,0 +1,204 @@ +# Copyright 2017 ARM Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from collections import OrderedDict +import logging +import re + +from devlib.instrument import (Instrument, INSTANTANEOUS, + Measurement, MeasurementType) +from devlib.exception import TargetError + +# Each entry in schedstats has a space-separated list of fields. DOMAIN_MEASURES +# and CPU_MEASURES are the fields for domain entries and CPU entries +# resepectively. +# +# See kernel/sched/stat.c and Documentation/scheduler/sched-stats.txt +# +# The names used here are based on the identifiers in the scheduler code. + +# Some domain fields are repeated for each idle type +DOMAIN_MEASURES = [] +for idle_type in ['CPU_IDLE', 'CPU_NOT_IDLE', 'CPU_NEWLY_IDLE']: + for lb_measure in [ + 'lb_count', + 'lb_balanced', + 'lb_failed', + 'lb_imbalance', + 'lb_gained', + 'lb_hot_gained', + 'lb_nobusyq', + 'lb_nobusyg']: + DOMAIN_MEASURES.append('{}:{}'.format(lb_measure, idle_type)) + +DOMAIN_MEASURES += [ + 'alb_count', + 'alb_failed', + 'alb_pushed', + 'sbe_count', + 'sbe_balanced', + 'sbe_pushed', + 'sbf_count', + 'sbf_balanced', + 'sbf_pushed', + 'ttwu_wake_remote', + 'ttwu_move_affine', + 'ttwu_move_balance' +] + +CPU_MEASURES = [ + 'yld_count', + 'legacy_always_zero', + 'schedule_count', + 'sched_goidle', + 'ttwu_count', + 'ttwu_local', + 'rq_cpu_time', + 'run_delay', + 'pcount' +] + +class SchedstatsInstrument(Instrument): + """ + An instrument for parsing Linux's schedstats + + Creates a *site* for each CPU and each sched_domain (i.e. for each line of + /proc/schedstat), and a *channel* for each item in the schedstats file. For + example a *site* named "cpu0" will be created for the scheduler stats on + CPU0 and a *site* named "cpu0domain0" will be created for the scheduler + stats on CPU0's first-level scheduling domain. + + For example: + + - If :method:`reset` is called with ``sites=['cpu0']`` then all + stats will be collected for CPU0's runqueue, with a channel for each + statistic. + + - If :method:`reset` is called with ``kinds=['alb_pushed']`` then the count + of migrations successfully triggered by active_load_balance will be + colelcted for each sched domain, with a channel for each domain. + + The measurements are named according to corresponding identifiers in the + kernel scheduler code. The names for ``sched_domain.lb_*`` stats, which are + recorded per ``cpu_idle_type`` are suffixed with a ':' followed by the idle + type, for example ``'lb_balanced:CPU_NEWLY_IDLE'``. + + Only supports schedstats version 15. + + Only supports the CPU and domain data in /proc/schedstat, not the per-task + data under /proc//schedstat. + """ + + mode = INSTANTANEOUS + + sysctl_path = '/proc/sys/kernel/sched_schedstats' + schedstat_path = '/proc/schedstat' + + def __init__(self, *args, **kwargs): + super(SchedstatsInstrument, self).__init__(*args, **kwargs) + self.logger = logging.getLogger(self.__class__.__name__) + + try: + self.old_sysctl_value = self.target.read_int(self.sysctl_path) + except TargetError: + if not self.target.file_exists(self.sysctl_path): + raise TargetError('schedstats not supported by target. ' + 'Ensure CONFIG_SCHEDSTATS is enabled.') + + self.target.write_value(self.sysctl_path, 1) + + # Check version matches + lines = self.target.read_value(self.schedstat_path).splitlines() + match = re.search(r'version ([0-9]+)', lines[0]) + if not match or match.group(1) != '15': + raise TargetError( + 'Unsupported schedstat version string: "{}"'.format(lines[0])) + + # Take a sample of the schedstat file to figure out which channels to + # create. + # We'll create a site for each CPU and a site for each sched_domain. + for site, measures in self._get_sample().iteritems(): + if site.startswith('cpu'): + measurement_category = 'schedstat_cpu' + else: + measurement_category = 'schedstat_domain' + + for measurement_name in measures.keys(): + # TODO: constructing multiple MeasurementTypes with same + # params. Does it matter? + measurement_type = MeasurementType( + measurement_name, '', measurement_category) + self.add_channel(site=site, + name='{}_{}'.format(site, measurement_name), + measure=measurement_type) + + def teardown(self): + self.target.write_value(self.sysctl_path, self.old_sysctl_value) + + def _get_sample(self): + lines = self.target.read_value(self.schedstat_path).splitlines() + ret = OrderedDict() + + # Example /proc/schedstat contents: + # + # version 15 + # timestamp + # cpu0 + # domain0 + # domain1 + # cpu1 + # domain0 + # domain1 + + curr_cpu = None + for line in lines[2:]: + tokens = line.split() + if tokens[0].startswith('cpu'): + curr_cpu = tokens[0] + site = curr_cpu + measures = CPU_MEASURES + tokens = tokens[1:] + elif tokens[0].startswith('domain'): + if not curr_cpu: + raise TargetError( + 'Failed to parse schedstats, found domain before CPU') + # We'll name the site for the domain like "cpu0domain0" + site = curr_cpu + tokens[0] + measures = DOMAIN_MEASURES + tokens = tokens[2:] + else: + self.logger.warning( + 'Unrecognised schedstats line: "{}"'.format(line)) + continue + + values = [int(t) for t in tokens] + if len(values) != len(measures): + raise TargetError( + 'Unexpected length for schedstat line "{}"'.format(line)) + ret[site] = OrderedDict(zip(measures, values)) + + return ret + + def take_measurement(self): + ret = [] + sample = self._get_sample() + + for channel in self.active_channels: + value = sample[channel.site][channel.kind] + ret.append(Measurement(value, channel)) + + return ret + +