From 6b3b5310841cdb03ecffcbc648ded5a66469e3f3 Mon Sep 17 00:00:00 2001 From: Michele Di Giorgio Date: Tue, 10 Oct 2017 19:08:07 +0100 Subject: [PATCH 1/4] tests: load_tracking: add duty_cycle parameter to get_wload Adding a parameter to specify different duty cycles to get_wload. This allows to have tests that use different types of workloads. --- tests/eas/load_tracking.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/tests/eas/load_tracking.py b/tests/eas/load_tracking.py index 989c57137..7550de13a 100644 --- a/tests/eas/load_tracking.py +++ b/tests/eas/load_tracking.py @@ -59,16 +59,23 @@ class _LoadTrackingBase(LisaTest): super(_LoadTrackingBase, cls).runExperiments(*args, **kwargs) @classmethod - def get_wload(cls, cpu): + def get_wload(cls, cpu, duty_cycle_pct): """ - Get a specification for a 10% rt-app workload, pinned to the given CPU + Get a specification for a rt-app workload with the specificied duty + cycle, pinned to the given CPU. + + :param cpu: CPU where to pin the task + :type cpu: int + + :param duty_cycle_pct: duty cycle of the workload + :type duty_cycle_pct: int """ return { 'type' : 'rt-app', 'conf' : { 'class' : 'periodic', 'params' : { - 'duty_cycle_pct': 10, + 'duty_cycle_pct': duty_cycle_pct, 'duration_s': 1, 'period_ms': 16, }, @@ -197,7 +204,7 @@ class FreqInvarianceTest(_LoadTrackingBase): key=lambda c: cls._get_cpu_capacity(test_env, c)) wloads = { - 'fie_10pct' : cls.get_wload(cpu) + 'fie_10pct' : cls.get_wload(cpu, 10) } # Create a set of confs with different frequencies @@ -291,7 +298,7 @@ class CpuInvarianceTest(_LoadTrackingBase): # No need to test on every CPU, just one for each capacity value continue tested_caps.add(cap) - wloads['cie_cpu{}'.format(cpu)] = cls.get_wload(cpu) + wloads['cie_cpu{}'.format(cpu)] = cls.get_wload(cpu, 10) conf = { 'tag' : 'cie_conf', -- GitLab From bf34fee18659ee23219a01f6054a3065b18bf8d2 Mon Sep 17 00:00:00 2001 From: Michele Di Giorgio Date: Tue, 17 Oct 2017 17:03:17 +0100 Subject: [PATCH 2/4] tests: load_tracking: add PELT tests for tasks load tracking Add a couple of PELT tests that check util_avg and load_avg of a task. For this purpose we exploit the PELT simulator available in BART and compare the results of simulation with the actual signal extracted from the trace. In particular, the two tests check that the extracted signal's min, max and mean values in the stable range correspond to the values of the simulated signal. The second test looks at the behaviour of the signal and checks that it corresponds to the one of the simulated signal. --- libs/utils/test.py | 8 ++ tests/eas/load_tracking.py | 207 ++++++++++++++++++++++++++++++++++++- 2 files changed, 211 insertions(+), 4 deletions(-) diff --git a/libs/utils/test.py b/libs/utils/test.py index 702de6dcc..29e29051e 100644 --- a/libs/utils/test.py +++ b/libs/utils/test.py @@ -177,6 +177,14 @@ class LisaTest(unittest.TestCase): start_times_dict = self.get_multi_assert(experiment).getStartTime() return min([t["starttime"] for t in start_times_dict.itervalues()]) + def get_task_start_time(self, experiment, task): + """ + Get the time at which the input task of the experiment workload began + executing + """ + start_times_dict = self.get_multi_assert(experiment, task_filter=task).getStartTime() + return min([t["starttime"] for t in start_times_dict.itervalues()]) + def get_end_time(self, experiment): """ Get the time at which the experiment workload finished executing diff --git a/tests/eas/load_tracking.py b/tests/eas/load_tracking.py index 7550de13a..058e8ed70 100644 --- a/tests/eas/load_tracking.py +++ b/tests/eas/load_tracking.py @@ -15,18 +15,21 @@ # limitations under the License. # -from bart.common.Utils import select_window, area_under_curve -from devlib.utils.misc import memoized -from trappy.stats.grammar import Parser import pandas as pd +from bart.common.Utils import select_window, area_under_curve +from bart.sched import pelt +from devlib.utils.misc import memoized from test import LisaTest, experiment_test +from trappy.stats.grammar import Parser UTIL_SCALE = 1024 # Time in seconds to allow for util_avg to converge (i.e. ignored time) UTIL_AVG_CONVERGENCE_TIME = 0.3 # Allowed margin between expected and observed util_avg value ERROR_MARGIN_PCT = 15 +# PELT half-life value in ms +HALF_LIFE_MS = 32 class _LoadTrackingBase(LisaTest): """Base class for shared functionality of load tracking tests""" @@ -76,7 +79,7 @@ class _LoadTrackingBase(LisaTest): 'class' : 'periodic', 'params' : { 'duty_cycle_pct': duty_cycle_pct, - 'duration_s': 1, + 'duration_s': 2, 'period_ms': 16, }, 'tasks' : 1, @@ -172,6 +175,25 @@ class _LoadTrackingBase(LisaTest): signal = select_window(signal, window) return area_under_curve(signal) / (window[1] - window[0]) + def get_simulated_pelt(self, experiment, task, init_value): + """ + Get simulated PELT signal and the periodic task used to model it. + + :returns: tuple of + - :mod:`bart.sched.pelt.Simulator` the PELT simulator object + - :mod:`bart.sched.pelt.PeriodicTask` simulated periodic task + - :mod:`pandas.DataFrame` instance which reports the computed + PELT values at each PELT sample interval. + """ + phase = experiment.wload.params['profile'][task]['phases'][0] + pelt_task = pelt.PeriodicTask(period_samples=phase.period_ms, + duty_cycle_pct=phase.duty_cycle_pct) + peltsim = pelt.Simulator(init_value=init_value, + half_life_ms=HALF_LIFE_MS) + df = peltsim.getSignal(pelt_task, 0, phase.duration_s + 1) + return peltsim, pelt_task, df + + class FreqInvarianceTest(_LoadTrackingBase): """ Goal @@ -330,3 +352,180 @@ class CpuInvarianceTest(_LoadTrackingBase): Test that the mean of the util_avg signal matched the expected value """ return self._test_signal(experiment, tasks, 'util_avg') + +class PELTTasksTest(_LoadTrackingBase): + """ + Goal + ==== + Basic checks for tasks related PELT signals behaviour. + + Detailed Description + ==================== + This test runs a synthetic periodic task on a CPU in the system and + collects a trace from the target device. The util_avg values are extracted + from scheduler trace events and the behaviour of the signal is compared + against a simulated value of PELT. + This class runs the following tests: + + - test_util_avg_range: test that util_avg's stable range matches with the + stable range of the simulated signal. In particular, this test compares + min, max and mean values of the two signals. + + - test_util_avg_behaviour: check behaviour of util_avg against the simualted + PELT signal. This test assumes that PELT is configured with 32 ms half + life time and the samples are 1024 us. Also, it assumes that the first + trace event related to the task used for testing is generated 'after' + the task starts (hence, we compute the initial PELT value when the task + started). + + Expected Behaviour + ================== + Simulated PELT signal and the signal extracted from the trace should have + very similar min, max and mean values in the stable range and the behaviour of + the signal should be very similar to simulated one. + """ + + @classmethod + def _getExperimentsConf(cls, test_env): + # Run the 50% workload on a CPU with highest capacity + target_cpu = min(test_env.calibration(), + key=test_env.calibration().get) + + wloads = { + 'pelt_behv' : cls.get_wload(target_cpu, 50) + } + + conf = { + 'tag' : 'pelt_behv_conf', + 'flags' : ['ftrace', 'freeze_userspace'], + 'cpufreq' : {'governor' : 'performance'}, + } + + return { + 'wloads': wloads, + 'confs': [conf], + } + + def _test_range(self, experiment, tasks, signal_name): + [task] = tasks + signal_df = self.get_sched_task_signals(experiment, [signal_name]) + # Get stats and stable range of the simulated PELT signal + start_time = self.get_task_start_time(experiment, task) + init_pelt = pelt.Simulator.estimateInitialPeltValue( + signal_df[signal_name].iloc[0], signal_df.index[0], + start_time, HALF_LIFE_MS + ) + peltsim, pelt_task, sim_df = self.get_simulated_pelt(experiment, + task, + init_pelt) + sim_range = peltsim.stableRange(pelt_task) + stable_time = peltsim.stableTime(pelt_task) + window = (start_time + stable_time, + start_time + stable_time + 0.5) + # Get signal statistics in a period of time where the signal is + # supposed to be stable + signal_stats = signal_df[window[0]:window[1]][signal_name].describe() + + # Narrow down simulated PELT signal to stable period + sim_df = sim_df[window[0]:window[1]].pelt_value + + # Check min + error_margin = sim_range.min_value * (ERROR_MARGIN_PCT / 100.) + msg = 'Stable range min value around {}, expected {}'.format( + signal_stats['min'], sim_range.min_value) + self.assertAlmostEqual(sim_range.min_value, signal_stats['min'], + delta=error_margin, msg=msg) + + # Check max + error_margin = sim_range.max_value * (ERROR_MARGIN_PCT / 100.) + msg = 'Stable range max value around {}, expected {}'.format( + signal_stats['max'], sim_range.max_value) + self.assertAlmostEqual(sim_range.max_value, signal_stats['max'], + delta=error_margin, msg=msg) + + # Check mean + sim_mean = sim_df.mean() + error_margin = sim_mean * (ERROR_MARGIN_PCT / 100.) + msg = 'Saw mean value of around {}, expected {}'.format( + signal_stats['mean'], sim_mean) + self.assertAlmostEqual(sim_mean, signal_stats['mean'], + delta=error_margin, msg=msg) + + def _test_behaviour(self, experiment, tasks, signal_name): + [task] = tasks + signal_df = self.get_sched_task_signals(experiment, [signal_name]) + # Get instant of time when the task starts running + start_time = self.get_task_start_time(experiment, task) + + # Get information about the task + phase = experiment.wload.params['profile'][task]['phases'][0] + + # Create simulated PELT signal for a periodic task + init_pelt = pelt.Simulator.estimateInitialPeltValue( + signal_df[signal_name].iloc[0], signal_df.index[0], + start_time, HALF_LIFE_MS + ) + peltsim, pelt_task, sim_df = self.get_simulated_pelt( + experiment, task, init_pelt + ) + + # Compare actual PELT signal with the simulated one + margin = 0.05 + period_s = phase.period_ms / 1e3 + sim_period_ms = phase.period_ms * (peltsim._sample_us / 1e6) + n_errors = 0 + for entry in signal_df.iterrows(): + trace_val = entry[1][signal_name] + timestamp = entry[0] - start_time + # Next two instructions map the trace timestamp to a simulated + # signal timestamp. This is due to the fact that the 1 ms is + # actually 1024 us in the simulated signal. + n_periods = timestamp / period_s + nearest_timestamp = n_periods * sim_period_ms + sim_val_loc = sim_df.index.get_loc(nearest_timestamp, + method='nearest') + sim_val = sim_df.pelt_value.iloc[sim_val_loc] + if trace_val > (sim_val * (1 + margin)) or \ + trace_val < (sim_val * (1 - margin)): + self._log.debug("At {} trace shows {}={}" + .format(entry[0], signal_name, trace_val)) + self._log.debug("At ({}, {}) simulation shows {}={}" + .format(sim_df.index[sim_val_loc], + signal_name, + sim_val_loc, + sim_val)) + n_errors += 1 + + msg = "Total number of errors: {}/{}".format(n_errors, len(signal_df)) + # Exclude possible outliers (these may be due to a kernel thread that + # for some reason gets coscheduled with our workload). + self.assertLess(n_errors/len(signal_df), margin, msg) + + @experiment_test + def test_util_avg_range(self, experiment, tasks): + """ + Test util_avg stable range for a 50% periodic task + """ + return self._test_range(experiment, tasks, 'util_avg') + + @experiment_test + def test_util_avg_behaviour(self, experiment, tasks): + """ + Test util_avg behaviour for a 50% periodic task + """ + return self._test_behaviour(experiment, tasks, 'util_avg') + + @experiment_test + def test_load_avg_range(self, experiment, tasks): + """ + Test load_avg stable range for a 50% periodic task + """ + return self._test_range(experiment, tasks, 'load_avg') + + @experiment_test + def test_load_avg_behaviour(self, experiment, tasks): + """ + Test load_avg behaviour for a 50% periodic task + """ + return self._test_behaviour(experiment, tasks, 'load_avg') + -- GitLab From d0de9c390096398be41622d6ead1db96f9d43401 Mon Sep 17 00:00:00 2001 From: Michele Di Giorgio Date: Thu, 30 Nov 2017 10:33:05 +0000 Subject: [PATCH 3/4] wlgen: add SchedEntity data structure This is a data structure that can be used for creating hierarchies of sched entities, namely tasks and taskgroups. It makes it easy to navigate through the hierarchy, create workloads and run them in the specified Cgroup. The hierarchy is represented as a tree where tasks can only be leaf nodes, whereas taskgroup can have children, either tasks or taskgroups. --- libs/wlgen/wlgen/utils.py | 206 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 libs/wlgen/wlgen/utils.py diff --git a/libs/wlgen/wlgen/utils.py b/libs/wlgen/wlgen/utils.py new file mode 100644 index 000000000..53ec3449c --- /dev/null +++ b/libs/wlgen/wlgen/utils.py @@ -0,0 +1,206 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright (C) 2016, ARM Limited and contributors. +# +# 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 wlgen import RTA, Periodic + +class SchedEntity(object): + """ + A sched entity can be either a task or a taskgroup. If it is a task, then + it has no children. A taskgroup, instead can have several children nodes + being tasks or task groups. + + :param se_type: Sched entity type, either "task" or "tg" (taskgroup) + :type se_type: str + """ + + def __init__(self, se_type): + allowed_se_types = ['task', 'tg'] + if se_type not in allowed_se_types: + raise ValueError('Invalid sched entity type. Allowed values are {}' + .format(allowed_se_types)) + + self._se_type = se_type + self.parent = None + self.children = set() + + def add_children(self, ses): + raise NotImplementedError("add_children() must be implemented") + + def _iter(self): + yield self + for child in self.children: + for child_i in child._iter(): + yield child_i + + def iter_nodes(self): + """Pre-order traversal of all nodes""" + return self._iter() + + @property + def is_task(self): + return self._se_type == 'task' + + def get_expected_util(self): + raise NotImplementedError("get_expected_util() must be implemented") + + def print_hieararchy(self, level=0): + """In-order visualization of the tree""" + if level > 0: + print " " * (level) + "|--" + self.name + else: + print self.name + + for child in self.children: + child.visit(level + 1) + +class Task(SchedEntity): + """ + Task Entity class + + :param name: Name of the task. + :type name: str + + :param test_env: Test environment. + :type test_env: env.TestEnv + + :param cpus: List of CPUs the workload can run on. + :type cpus: list(int) + + :param period_ms: Period of each task in milliseconds. + :type period_ms: int + + :param duty_cycle_pct: Dduty cycle of the periodic workload. + Default 50% + :type duty_cycle_pct: int + + :param duration_s: Total duration of the workload. Default 1 second. + :type duration_s: int + + :param kind: Type of RTA workload. Can be 'profile' or 'custom'. + Default value is 'profile'. + :type kind: str + + :param num_tasks: Number of tasks to spawn. + :type num_tasks: int + """ + + def __init__(self, name, test_env, cpus, period_ms=100, duty_cycle_pct=50, + duration_s=1, kind='profile', num_tasks=1): + super(Task, self).__init__("task") + + self.name = name + self.period_ms = period_ms + self.duty_cycle_pct = duty_cycle_pct + self.duration_s = duration_s + self.cpus = cpus + allowed_kinds = ['profile', 'custom'] + if kind not in allowed_kinds: + raise ValueError('{} not allowed, kind can be one of {}' + .format(kind, allowed_kinds)) + self.kind = kind + self.num_tasks = num_tasks + + # Create rt-app workload + t = Periodic(period_ms=period_ms, + duty_cycle_pct=duty_cycle_pct, + duration_s=duration_s).get() + self.wload = RTA(test_env.target, name, test_env.calibration()) + if num_tasks > 1: + conf_params = {name + "_{}".format(i): t for i in xrange(num_tasks)} + else: + conf_params = {name: t} + self.wload.conf(kind=kind, + params=conf_params, + run_dir=test_env.target.working_directory) + + def __repr__(self): + return "Task: " + self.name + + def add_children(self, ses): + raise TypeError('Cannot add children entities to a task entity.') + + def get_expected_util(self): + """ + Get expected utilization value. For tasks this corresponds to + corresponds to the duty cycle of the task. + + :returns: int - expected utilization of the task + """ + return 1024 * (self.duty_cycle_pct * self.num_tasks / 100.0) + +class Taskgroup(SchedEntity): + """ + Taskgroup class + + Create the taskgroup object and instantiate the relative Cgroup on the + target platform. Notice that in order to set attributes of a Cgroup, + the attributes of its parents have to be set in the first place. + Therefore, those should be instantieted first. + + :param name: Absolute path to the taskgroup starting from root '/' + :type name: str + + :param cpus: List of CPUs associated to this taskgroup + :type cpus: list(int) + + :param mems: Set cgroup mems attribute to the specified value + :type mems: int + + :param test_env: Test environment + :type test_env: env.TestEnv + """ + + def __init__(self, name, cpus, mems, test_env): + super(Taskgroup, self).__init__("tg") + + self.name = name + self.cpus = cpus + self.mems = mems + + # Create Cgroup + cnt = test_env.target.cgroups.controller('cpuset') + cgp = cnt.cgroup(name) + cgp.set(cpus=cpus, mems=mems) + + cnt = test_env.target.cgroups.controller('cpu') + cgp = cnt.cgroup(name) + + def __repr__(self): + return "Taskgroup: " + self.name + + def add_children(self, ses): + """ + Add the specified sched entities as children of the current one. + + :param ses: sched entity to be added, either Task or Taskgroup + :type ses: list(SchedEntity) + """ + self.children.update(ses) + for entity in ses: + entity.parent = self + + def get_expected_util(self): + """ + Get expected utilization value. If the sched entity is a task, then + this corresponds to corresponds to the duty cycle of the task. In case + of task group, this is the sum of the utilizations of its children. + """ + util = 0.0 + for child in self.children: + util += child.get_expected_util() + return util + -- GitLab From dfa6b84cdf8ba1bae5d9372244a144e978c8075f Mon Sep 17 00:00:00 2001 From: Michele Di Giorgio Date: Thu, 30 Nov 2017 11:10:39 +0000 Subject: [PATCH 4/4] tests: load_tracking: add PELT tests for taskgroups utilization This introduces a SchedEntity class which is a tree of tasks and taskgroups. Tasks can only be leaves, whereas taskgroups can only be nodes. The data structure makes it easy to come up with new hierarchies of tasks and taskgroups and test that PELT utilization is correctly propagated through the taskgroups. --- tests/eas/load_tracking.py | 371 ++++++++++++++++++++++++++++++++++++- 1 file changed, 370 insertions(+), 1 deletion(-) diff --git a/tests/eas/load_tracking.py b/tests/eas/load_tracking.py index 058e8ed70..61b2215ff 100644 --- a/tests/eas/load_tracking.py +++ b/tests/eas/load_tracking.py @@ -15,13 +15,20 @@ # limitations under the License. # +import os import pandas as pd +import logging from bart.common.Utils import select_window, area_under_curve from bart.sched import pelt from devlib.utils.misc import memoized +from env import TestEnv +from executor import Executor from test import LisaTest, experiment_test +from time import sleep +from trace import Trace from trappy.stats.grammar import Parser +from wlgen.utils import SchedEntity, Task, Taskgroup UTIL_SCALE = 1024 # Time in seconds to allow for util_avg to converge (i.e. ignored time) @@ -41,7 +48,8 @@ class _LoadTrackingBase(LisaTest): 'sched_load_avg_task', 'sched_load_avg_cpu', 'sched_pelt_se', - 'sched_load_se' + 'sched_load_se', + 'sched_load_cfs_rq', ], }, # cgroups required by freeze_userspace flag @@ -529,3 +537,364 @@ class PELTTasksTest(_LoadTrackingBase): """ return self._test_behaviour(experiment, tasks, 'load_avg') +class _PELTTaskGroupsTest(LisaTest): + """ + Abstract base class for generic tests on PELT taskgroups signals + + Subclasses should provide: + - .tasks_conf member to generate the rt-app synthetics. + - .target_cpu CPU where the tasks should run + - .trace object where to save the parsed trace + """ + test_conf = { + 'tools' : [ 'rt-app' ], + 'ftrace' : { + 'events' : [ + 'sched_switch', + 'sched_load_avg_task', + 'sched_load_avg_cpu', + 'sched_pelt_se', + 'sched_load_se', + 'sched_load_cfs_rq', + ], + }, + # cgroups required by freeze_userspace flag + 'modules': ['cpufreq', 'cgroups'], + } + # Allowed error margin + allowed_util_margin = 0.02 + + @classmethod + def runExperiments(cls): + """ + Set up logging and trigger running experiments + """ + cls._log = logging.getLogger('LisaTest') + + cls._log.info('Setup tests execution engine...') + te = TestEnv(test_conf=cls._getTestConf()) + + experiments_conf = cls._getExperimentsConf(te) + test_dir = os.path.join(te.res_dir, experiments_conf['confs'][0]['tag']) + os.makedirs(test_dir) + + # Setting cpufreq governor to performance + te.target.cpufreq.set_all_governors('performance') + + # Creating cgroups hierarchy + cpuset_cnt = te.target.cgroups.controller('cpuset') + cpu_cnt = te.target.cgroups.controller('cpu') + + max_duration = 0 + for se in cls.root_group.iter_nodes(): + if se.is_task: + max_duration = max(max_duration, se.duration_s) + + # Freeze userspace tasks + cls._log.info('Freezing userspace tasks') + te.target.cgroups.freeze(Executor.critical_tasks[te.target.os]) + + cls._log.info('FTrace events collection enabled') + te.ftrace.start() + + # Run tasks + cls._log.info('Running the tasks') + # Run all tasks in background and wait for completion + for se in cls.root_group.iter_nodes(): + if se.is_task: + # Run tasks + se.wload.run(out_dir=test_dir, cpus=se.cpus, + cgroup=se.parent.name, background=True) + sleep(max_duration) + + te.ftrace.stop() + + trace_file = os.path.join(test_dir, 'trace.dat') + te.ftrace.get_trace(trace_file) + cls._log.info('Collected FTrace binary trace: %s', trace_file) + + # Un-freeze userspace tasks + cls._log.info('Un-freezing userspace tasks') + te.target.cgroups.freeze(thaw=True) + + # Extract trace + cls.trace = Trace(None, test_dir, te.ftrace.events) + + def _test_group_util(self, group): + if 'sched_load_se' not in self.trace.available_events: + raise ValueError('No sched_load_se events. ' + 'Does the kernel support them?') + if 'sched_load_cfs_rq' not in self.trace.available_events: + raise ValueError('No sched_load_cfs_rq events. ' + 'Does the kernel support them?') + if 'sched_switch' not in self.trace.available_events: + raise ValueError('No sched_switch events. ' + 'Does the kernel support them?') + + task_util_df = self.trace.data_frame.trace_event('sched_load_se') + tg_util_df = self.trace.data_frame.trace_event('sched_load_cfs_rq') + sw_df = self.trace.data_frame.trace_event('sched_switch') + + tg = None + for se in self.root_group.iter_nodes(): + if se.name == group: + tg = se + + if tg is None: + raise ValueError('{} taskgroup does not exist.'.format(group)) + + # Only consider the time interval where the signal should be stable + tasks_names = [se.name for se in tg.iter_nodes() if se.is_task] + tasks_sw_df = sw_df[sw_df.next_comm.isin(tasks_names)] + start = tasks_sw_df.index[0] + UTIL_AVG_CONVERGENCE_TIME + end = tasks_sw_df.index[-1] - UTIL_AVG_CONVERGENCE_TIME + task_util_df = task_util_df[start:end] + tg_util_df = tg_util_df[start:end] + + # Compute mean util of the taskgroup and its children + util_tg = tg_util_df[(tg_util_df.path == group) & + (tg_util_df.cpu == self.target_cpu)].util + util_mean_tg = area_under_curve(util_tg) / (end - start) + + msg = 'Saw util {} for {} cgroup, expected {}' + expected_trace_util = 0.0 + for child in tg.children: + if child.is_task: + util_s = task_util_df[task_util_df.comm == child.name].util + else: + util_s = tg_util_df[(tg_util_df.path == child.name) & + (tg_util_df.cpu == self.target_cpu)].util + + util_mean = area_under_curve(util_s) / (end - start) + # Make sure the trace utilization of children entities matches the + # expected utilization (i.e. duty cycle for tasks, sum of utils for + # taskgroups) + expected = child.get_expected_util() + error_margin = expected * (ERROR_MARGIN_PCT / 100.) + self.assertAlmostEqual(util_mean, expected, + delta=error_margin, + msg=msg.format(util_mean, + child.name, + expected)) + + expected_trace_util += util_mean + + error_margin = expected_trace_util * self.allowed_util_margin + self.assertAlmostEqual(util_mean_tg, expected_trace_util, + delta=error_margin, + msg=msg.format(util_mean_tg, + group, + expected_trace_util)) + +class TwoGroupsCascade(_PELTTaskGroupsTest): + """ + Test PELT utilization for task groups on the following hierarchy: + + +-----+ + | "/" | + +-----+ + / \ + / \ + +------+ t0_1 + |"/tg1"| + +------+ + / \ + / \ + t1_1 +------------+ + |"/tg1/tg1_1"| + +------------+ + / \ + / \ + t2_1 t2_2 + + """ + target_cpu = 0 + root_group = None + trace = None + + @classmethod + def _getExperimentsConf(cls, test_env): + # Run all workloads on a CPU with highest capacity + cls.target_cpu = min(test_env.calibration(), + key=test_env.calibration().get) + + # Create taskgroups + cpus = test_env.target.list_online_cpus() + mems = 0 + cls.root_group = Taskgroup("/", cpus, mems, test_env) + tg1 = Taskgroup("/tg1", cpus, mems, test_env) + tg1_1 = Taskgroup("/tg1/tg1_1", cpus, mems, test_env) + + # Create tasks + period_ms = 16 + duty_cycle_pct = 10 + duration_s = 3 + cpus = [cls.target_cpu] + t2_1 = Task("t2_1", test_env, + cpus, period_ms=period_ms, + duty_cycle_pct=duty_cycle_pct, duration_s=duration_s) + t2_2 = Task("t2_2", test_env, + cpus, period_ms=period_ms, + duty_cycle_pct=duty_cycle_pct, duration_s=duration_s) + t1_1 = Task("t1_1", test_env, + cpus, period_ms=period_ms, + duty_cycle_pct=duty_cycle_pct, duration_s=duration_s) + t0_1 = Task("t0_1", test_env, + cpus, period_ms=period_ms, + duty_cycle_pct=duty_cycle_pct, duration_s=duration_s) + + # Link nodes to the hierarchy tree + cls.root_group.add_children([t0_1, tg1]) + tg1.add_children([tg1_1, t1_1]) + tg1_1.add_children([t2_1, t2_2]) + + conf = { + 'tag' : 'cgp_cascade', + 'flags' : ['ftrace', 'freeze_userspace'], + 'cpufreq' : {'governor' : 'performance'}, + } + + return { + 'wloads': {}, + 'confs': [conf], + } + + @classmethod + def setUpClass(cls, *args, **kwargs): + super(TwoGroupsCascade, cls).runExperiments(*args, **kwargs) + + def test_util_root_group(self): + """ + Test utilization propagation to cgroup root + """ + return self._test_group_util('/') + + def test_util_tg1_group(self): + """ + Test utilization propagation to cgroup /tg1 + """ + return self._test_group_util('/tg1') + + def test_util_tg1_1_group(self): + """ + Test utilization propagation to cgroup /tg1/tg1_1 + """ + return self._test_group_util('/tg1/tg1_1') + +class UnbalancedHierarchy(_PELTTaskGroupsTest): + """ + Test PELT utilization for task groups on the following hierarchy: + + +-----+ + | "/" | + +-----+ + / \ + / \ + +------+ t0_1 + |"/tg1"| + +------+ + / + / + +----------+ + |"/tg1/tg2"| + +----------+ + / \ + / \ + +--------------+ t2_1 + |"/tg1/tg2/tg3"| + +--------------+ + / + / + +------------------+ + |"/tg1/tg2/tg3/tg4"| + +------------------+ + / + / + t4_1 + + """ + target_cpu = 0 + root_group = None + trace = None + + @classmethod + def _getExperimentsConf(cls, test_env): + # Run all workloads on a CPU with highest capacity + cls.target_cpu = min(test_env.calibration(), + key=test_env.calibration().get) + + # Create taskgroups + cpus = test_env.target.list_online_cpus() + mems = 0 + cls.root_group = Taskgroup("/", cpus, mems, test_env) + tg1 = Taskgroup("/tg1", cpus, mems, test_env) + tg2 = Taskgroup("/tg1/tg2", cpus, mems, test_env) + tg3 = Taskgroup("/tg1/tg2/tg3", cpus, mems, test_env) + tg4 = Taskgroup("/tg1/tg2/tg3/tg4", cpus, mems, test_env) + + # Create tasks + period_ms = 16 + duty_cycle_pct = 10 + duration_s = 3 + cpus = [cls.target_cpu] + t0_1 = Task("t0_1", test_env, + cpus, period_ms=period_ms, + duty_cycle_pct=duty_cycle_pct, duration_s=duration_s) + t2_1 = Task("t2_1", test_env, + cpus, period_ms=period_ms, + duty_cycle_pct=duty_cycle_pct, duration_s=duration_s) + t4_1 = Task("t4_1", test_env, + cpus, period_ms=period_ms, + duty_cycle_pct=duty_cycle_pct, duration_s=duration_s) + + cls.root_group.add_children([t0_1, tg1]) + tg1.add_children([tg2]) + tg2.add_children([t2_1, tg3]) + tg3.add_children([tg4]) + tg4.add_children([t4_1]) + + conf = { + 'tag' : 'cgp_unbalanced', + 'flags' : ['ftrace', 'freeze_userspace'], + 'cpufreq' : {'governor' : 'performance'}, + } + + return { + 'wloads': {}, + 'confs': [conf], + } + + @classmethod + def setUpClass(cls, *args, **kwargs): + super(UnbalancedHierarchy, cls).runExperiments(*args, **kwargs) + + def test_util_root_group(self): + """ + Test utilization propagation to cgroup root + """ + return self._test_group_util('/') + + def test_util_tg1_group(self): + """ + Test utilization propagation to cgroup /tg1 + """ + return self._test_group_util('/tg1') + + def test_util_tg2_group(self): + """ + Test utilization propagation to cgroup /tg1/tg2 + """ + return self._test_group_util('/tg1/tg2') + + def test_util_tg3_group(self): + """ + Test utilization propagation to cgroup /tg1/tg2/tg3 + """ + return self._test_group_util('/tg1/tg2/tg3') + + def test_util_tg4_group(self): + """ + Test utilization propagation to cgroup /tg1/tg2/tg3/tg4 + """ + return self._test_group_util('/tg1/tg2/tg3/tg4') + -- GitLab