Skip to content

Commit 435ebf6

Browse files
author
Brendan Jackman
committed
instrument/schedstats: Add instrument for schedstats
This instrument collects snapshots of the CPU/sched_domain scheduler statistics in /proc/schedstat, and reports a Measure for each of them. It currently doesn't support the per-task stats reported in /proc/<pid>/schedstat, and it ignores the additional EAS schedstats reported by Android kernels. The measures are named according to the corresponding identifiers in the kernel source code. Although This might appear to be an obscure way to refer to them, the information in /proc/schedstats is not going to be any use to anyone who doesn't have the kernel code handy. There is an English description in Documentation/scheduler/sched-stats.txt but it can only really be considered a reference for those who already understand the scheduler. Some of the measures are representing time intervals, but their units are jiffies, the meaning of which depends on the kernel configuration. Therefore no units are reported for the measurements.
1 parent 857edbd commit 435ebf6

File tree

2 files changed

+225
-0
lines changed

2 files changed

+225
-0
lines changed

devlib/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from devlib.instrument.monsoon import MonsoonInstrument
1919
from devlib.instrument.netstats import NetstatsInstrument
2020
from devlib.instrument.gem5power import Gem5PowerInstrument
21+
from devlib.instrument.schedstats import SchedstatsInstrument
2122

2223
from devlib.derived import DerivedMeasurements, DerivedMetric
2324
from devlib.derived.energy import DerivedEnergyMeasurements

devlib/instrument/schedstats.py

+224
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
# Copyright 2017 ARM Limited
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
16+
from collections import OrderedDict
17+
import logging
18+
import re
19+
20+
from devlib.instrument import (Instrument, INSTANTANEOUS,
21+
Measurement, MeasurementType)
22+
from devlib.exception import TargetError
23+
24+
# Each entry in schedstats has a space-separated list of fields. DOMAIN_MEASURES
25+
# and CPU_MEASURES are the fields for domain entries and CPU entries
26+
# resepectively.
27+
#
28+
# See kernel/sched/stat.c and Documentation/scheduler/sched-stats.txt
29+
#
30+
# The names used here are based on the identifiers in the scheduler code.
31+
32+
# Some domain fields are repeated for each idle type
33+
DOMAIN_MEASURES = []
34+
for idle_type in ['CPU_IDLE', 'CPU_NOT_IDLE', 'CPU_NEWLY_IDLE']:
35+
for lb_measure in [
36+
'lb_count',
37+
'lb_balanced',
38+
'lb_failed',
39+
'lb_imbalance',
40+
'lb_gained',
41+
'lb_hot_gained',
42+
'lb_nobusyq',
43+
'lb_nobusyg']:
44+
DOMAIN_MEASURES.append('{}:{}'.format(lb_measure, idle_type))
45+
46+
DOMAIN_MEASURES += [
47+
'alb_count',
48+
'alb_failed',
49+
'alb_pushed',
50+
'sbe_count',
51+
'sbe_balanced',
52+
'sbe_pushed',
53+
'sbf_count',
54+
'sbf_balanced',
55+
'sbf_pushed',
56+
'ttwu_wake_remote',
57+
'ttwu_move_affine',
58+
'ttwu_move_balance'
59+
]
60+
61+
CPU_MEASURES = [
62+
'yld_count',
63+
'legacy_always_zero',
64+
'schedule_count',
65+
'sched_goidle',
66+
'ttwu_count',
67+
'ttwu_local',
68+
'rq_cpu_time',
69+
'run_delay',
70+
'pcount'
71+
]
72+
73+
class SchedstatsInstrument(Instrument):
74+
"""
75+
An instrument for parsing Linux's schedstats
76+
77+
Creates a *site* for each CPU and each sched_domain (i.e. for each line of
78+
/proc/schedstat), and a *channel* for each item in the schedstats file. For
79+
example a *site* named "cpu0" will be created for the scheduler stats on
80+
CPU0 and a *site* named "cpu0domain0" will be created for the scheduler
81+
stats on CPU0's first-level scheduling domain.
82+
83+
For example:
84+
85+
- If :method:`reset` is called with ``sites=['cpu0']`` then all
86+
stats will be collected for CPU0's runqueue, with a channel for each
87+
statistic.
88+
89+
- If :method:`reset` is called with ``kinds=['alb_pushed']`` then the count
90+
of migrations successfully triggered by active_load_balance will be
91+
colelcted for each sched domain, with a channel for each domain.
92+
93+
The measurements are named according to corresponding identifiers in the
94+
kernel scheduler code. The names for ``sched_domain.lb_*`` stats, which are
95+
recorded per ``cpu_idle_type`` are suffixed with a ':' followed by the idle
96+
type, for example ``'lb_balanced:CPU_NEWLY_IDLE'``.
97+
98+
Only supports schedstats version 15.
99+
100+
Only supports the CPU and domain data in /proc/schedstat, not the per-task
101+
data under /proc/<pid>/schedstat.
102+
"""
103+
104+
mode = INSTANTANEOUS
105+
106+
sysctl_path = '/proc/sys/kernel/sched_schedstats'
107+
schedstat_path = '/proc/schedstat'
108+
109+
def __init__(self, *args, **kwargs):
110+
super(SchedstatsInstrument, self).__init__(*args, **kwargs)
111+
self.logger = logging.getLogger(self.__class__.__name__)
112+
113+
# Check schedstats is present in the kernel and the format version
114+
# matches what we can parse.
115+
try:
116+
lines = self.target.read_value(self.schedstat_path).splitlines()
117+
except TargetError:
118+
if not self.target.file_exists(self.schedstat_path):
119+
raise TargetError('schedstats not supported by target. '
120+
'Ensure CONFIG_SCHEDSTATS is enabled.')
121+
raise
122+
123+
match = re.search(r'version ([0-9]+)', lines[0])
124+
if not match or match.group(1) != '15':
125+
raise TargetError(
126+
'Unsupported schedstat version string: "{}"'.format(lines[0]))
127+
128+
# On 4.6+ kernels, schedstats needs to be enabled via kernel cmdline or
129+
# sysctl. If not, we'll just get a load of zeroes.
130+
self.old_sysctl_value = None
131+
if self.target.kernel_version.parts >= (4, 6):
132+
if self.target.file_exists(self.sysctl_path):
133+
self.old_sysctl_value = self.target.read_int(self.sysctl_path)
134+
self.target.write_value(self.sysctl_path, 1)
135+
else:
136+
try:
137+
cmdline = self.target.read_value('/proc/cmdline')
138+
except TargetError:
139+
raise TargetError(
140+
"Couldn't verify that schedstats is enabled. "
141+
"Enabling CONFIG_PROC_SYSCTL will probably help")
142+
if "schedstats=enable" not in cmdline:
143+
raise TargetError(
144+
"schedstats is compiled into the kernel but not enabled at runtime. "
145+
"Enable CONFIG_PROC_SYSCTL or add schedstats=enable to the cmdline.")
146+
147+
# Take a sample of the schedstat file to figure out which channels to
148+
# create.
149+
# We'll create a site for each CPU and a site for each sched_domain.
150+
for site, measures in self._get_sample().iteritems():
151+
if site.startswith('cpu'):
152+
measurement_category = 'schedstat_cpu'
153+
else:
154+
measurement_category = 'schedstat_domain'
155+
156+
for measurement_name in measures.keys():
157+
measurement_type = MeasurementType(
158+
measurement_name, '', measurement_category)
159+
self.add_channel(site=site,
160+
measure=measurement_type)
161+
162+
def teardown(self):
163+
if self.old_sysctl_value is not None:
164+
self.target.write_value(self.sysctl_path, self.old_sysctl_value)
165+
166+
def _get_sample(self):
167+
lines = self.target.read_value(self.schedstat_path).splitlines()
168+
ret = OrderedDict()
169+
170+
# Example /proc/schedstat contents:
171+
#
172+
# version 15
173+
# timestamp <timestamp>
174+
# cpu0 <cpu fields>
175+
# domain0 <domain fields>
176+
# domain1 <domain fields>
177+
# cpu1 <cpu_fields>
178+
# domain0 <domain fields>
179+
# domain1 <domain fields>
180+
181+
curr_cpu = None
182+
for line in lines[2:]:
183+
tokens = line.split()
184+
if tokens[0].startswith('cpu'):
185+
curr_cpu = tokens[0]
186+
site = curr_cpu
187+
measures = CPU_MEASURES
188+
tokens = tokens[1:]
189+
elif tokens[0].startswith('domain'):
190+
if not curr_cpu:
191+
raise TargetError(
192+
'Failed to parse schedstats, found domain before CPU')
193+
# We'll name the site for the domain like "cpu0domain0"
194+
site = curr_cpu + tokens[0]
195+
measures = DOMAIN_MEASURES
196+
tokens = tokens[2:]
197+
elif tokens[0] == 'eas':
198+
# This line is added by EAS features. We don't yet parse it as
199+
# it might not be stable.
200+
continue
201+
else:
202+
self.logger.warning(
203+
'Unrecognised schedstats line: "%s', line)
204+
continue
205+
206+
values = [int(t) for t in tokens]
207+
if len(values) != len(measures):
208+
raise TargetError(
209+
'Unexpected length for schedstat line "%s"', line)
210+
ret[site] = OrderedDict(zip(measures, values))
211+
212+
return ret
213+
214+
def take_measurement(self):
215+
ret = []
216+
sample = self._get_sample()
217+
218+
for channel in self.active_channels:
219+
value = sample[channel.site][channel.kind]
220+
ret.append(Measurement(value, channel))
221+
222+
return ret
223+
224+

0 commit comments

Comments
 (0)