| # 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 bart.sched.SchedMultiAssert import SchedMultiAssert |
| from env import TestEnv |
| import json |
| import time |
| import trappy |
| import unittest |
| import os |
| from wlgen import Periodic, RTA |
| |
| # Read the config file and update the globals |
| CONF_FILE = os.path.join( |
| os.path.dirname( |
| os.path.abspath(__file__)), |
| "capacity_capping.config") |
| |
| with open(CONF_FILE, "r") as fh: |
| CONF_VARS = json.load(fh) |
| globals().update(CONF_VARS) |
| |
| class CapacityCappingTest(unittest.TestCase): |
| """ |
| Goal |
| ==== |
| |
| Verify that dynamic CPU capacity capping works in the system. |
| |
| Detailed Description |
| ==================== |
| |
| The maximum frequency of a core can be restricted to a lower value |
| than its absolute maximum frequency. This may happen because of |
| thermal management or as a request from userspace via sysfs. |
| Dynamic CPU capacity capping provides PELT and the scheduler CPU |
| capacity management with a maximum frequency scaling corrector |
| which describes the influence of running a CPU with a current |
| maximum frequency lower than the absolute maximum frequency. |
| |
| The test creates as many busy threads as there are big cpus. |
| These busy threads have high load and should run in the CPUs with |
| highest capacity. The test has three phases of equal length. In |
| the first phase, the system runs unconstrained. In the second |
| phase, the maximum frequency of the big cpus is limited to the |
| lowest frequency that the big frequency domain can run at. |
| Finally, in the third phase, the maximum frequency of the big cpus |
| is restored to its absolute maximum, i.e. the system is |
| unconstrained again. |
| |
| This test assumes that the lowest OPPs of the big cpus have less |
| capacity than the highest OPP of the little cpus. If that is not |
| the case, this test will fail. Arguably, capacity capping is not |
| needed in such a system. |
| |
| Expected Behaviour |
| ================== |
| |
| The threads have high load, so they should always run in the CPUs |
| with the highest capacity of the system. In the first phase the |
| system is unconstrained, so they should run on the big CPUs. In |
| the second phase, the big cluster's maximum frequency is limited |
| and the little CPUs have higher capacity. Therefore, in the |
| second phase of the test, the threads should migrate to the little |
| cpus. In the third phase the maximum frequency of the big cpus is |
| restored, so they become again the CPUs with the higest capacity |
| in the system. The busy threads must run on the big cpus in the |
| third phase. |
| |
| """ |
| |
| @classmethod |
| def setUpClass(cls): |
| cls.params = {} |
| cls.env = TestEnv(test_conf=TEST_CONF) |
| cls.trace_file = os.path.join(cls.env.res_dir, "cap_cap.dat") |
| cls.populate_params() |
| |
| cls.run_workload() |
| |
| trace = trappy.FTrace(cls.trace_file) |
| cls.sa = SchedMultiAssert(trace, cls.env.topology, |
| execnames=cls.params.keys()) |
| |
| @classmethod |
| def populate_params(cls): |
| for idx in range(len(cls.env.target.bl.bigs)): |
| task_name = "busy_thread{}".format(idx) |
| cls.params[task_name] = Periodic( |
| duty_cycle_pct=BIG_DCYCLE, |
| duration_s=WORKLOAD_DURATION_S, |
| period_ms=WORKLOAD_PERIOD_MS, |
| ).get() |
| |
| @classmethod |
| def run_workload(cls): |
| big_cpu = cls.env.target.bl.bigs[0] |
| big_cpufreq = "/sys/devices/system/cpu/cpu{}/cpufreq".format(big_cpu) |
| max_freq_path = os.path.join(big_cpufreq, "scaling_max_freq") |
| available_freqs_path = os.path.join(big_cpufreq, |
| "scaling_available_frequencies") |
| |
| available_freqs_str = cls.env.target.read_value(available_freqs_path) |
| available_freqs = available_freqs_str.split() |
| min_frequency = available_freqs[0] |
| max_frequency = available_freqs[-1] |
| |
| wload = RTA(cls.env.target, "busy_threads", |
| calibration=cls.env.calibration()) |
| wload.conf(kind="profile", params=cls.params) |
| phase_duration = WORKLOAD_DURATION_S / 3. |
| |
| cls.env.ftrace.start() |
| |
| wload.run(out_dir=cls.env.res_dir, background=True) |
| time.sleep(phase_duration) |
| |
| cls.env.target.write_value(max_freq_path, min_frequency) |
| time.sleep(phase_duration) |
| |
| cls.env.target.write_value(max_freq_path, max_frequency) |
| time.sleep(phase_duration) |
| |
| cls.env.ftrace.stop() |
| cls.env.ftrace.get_trace(cls.trace_file) |
| |
| def check_residencies(self, cpus, cpus_name, window, phase_description): |
| """Helper function to check the residencies of all busy threads on a |
| given set of cpus for a period of time.""" |
| |
| residency_dict = self.sa.getResidency("cluster", cpus, window=window, |
| percent=True) |
| |
| for pid, task_res in residency_dict.iteritems(): |
| msg = "Pid {} ran in {} cpus only {:.2f}% percent of the time when the system was {} (expected {:.2f}%)" \ |
| .format(pid, cpus_name, task_res["residency"], |
| phase_description, EXPECTED_BUSY_TIME_PCT) |
| |
| self.assertGreater(task_res["residency"], EXPECTED_BUSY_TIME_PCT, |
| msg) |
| |
| def test_tasks_starts_on_big(self): |
| """All busy threads run in the beginning in big cpus""" |
| |
| phase_duration = WORKLOAD_DURATION_S / 3. |
| unconstrained_window = (0, phase_duration) |
| self.check_residencies(self.env.target.bl.bigs, "big", |
| unconstrained_window, "unconstrained") |
| |
| def test_task_migrates_to_little_when_constrained(self): |
| """Busy threads migrate to little in the thermally constrained phase""" |
| |
| phase_duration = WORKLOAD_DURATION_S / 3. |
| migration_window = (phase_duration, phase_duration + MIGRATION_WINDOW) |
| num_tasks = len(self.params) |
| |
| msg = "One or more of the busy threads didn't migrate to a little cpu between {} and {}" \ |
| .format(migration_window[0], migration_window[1]) |
| self.assertTrue(self.sa.assertSwitch("cluster", self.env.target.bl.bigs, |
| self.env.target.bl.littles, |
| window=migration_window, |
| rank=num_tasks), |
| msg=msg) |
| |
| # The tasks must have migrated by the end of the |
| # migration_window and they should not move until the end of |
| # the phase. |
| constrained_window = (migration_window[1], 2 * phase_duration) |
| self.check_residencies(self.env.target.bl.littles, "little", |
| constrained_window, "thermally constrained") |
| |
| def test_task_returns_to_big_when_back_to_unconstrained(self): |
| """Busy threads return to big when system goes back to unconstrained |
| |
| In the last phase, when the frequency capping is released, busy threads |
| return to the big cpus""" |
| |
| phase_duration = WORKLOAD_DURATION_S / 3. |
| migration_window = (2 * phase_duration, |
| 2 * phase_duration + MIGRATION_WINDOW) |
| num_tasks = len(self.params) |
| |
| msg = "One of the busy threads didn't return to a big cpu" |
| self.assertTrue(self.sa.assertSwitch("cluster", |
| self.env.target.bl.littles, |
| self.env.target.bl.bigs, |
| window=migration_window, |
| rank=num_tasks), |
| msg=msg) |
| |
| # The tasks must have migrated by the end of the |
| # migration_window and they should continue to run on bigs |
| # until the end of the run. |
| last_phase = (migration_window[1], WORKLOAD_DURATION_S) |
| self.check_residencies(self.env.target.bl.bigs, "big", |
| last_phase, "unconstrained") |