crosperf: Setup CPU configuration based on 'cpu_usage'.

Based on dut_config['cpu_usage'] setup DUT to use
big or little cores only on ARM platform.

Ignore 'cpu_usage' if setup is invalid for targeted platform.
For example 'big_only" on Intel or ARM w/o big/little support.

BUG=chromium:966514
TEST=Unitest and local HW tests passed.

Cq-Depend: chromium:1778515
Change-Id: Ida457a1ceaf20c4dcbb8d40334a2423836999574
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/toolchain-utils/+/1783849
Reviewed-by: Caroline Tice <cmtice@chromium.org>
Legacy-Commit-Queue: Commit Bot <commit-bot@chromium.org>
Tested-by: Denis Nikitin <denik@chromium.org>
diff --git a/crosperf/suite_runner.py b/crosperf/suite_runner.py
index 2831b9b..d5d6c2c 100644
--- a/crosperf/suite_runner.py
+++ b/crosperf/suite_runner.py
@@ -8,6 +8,7 @@
 from __future__ import print_function
 
 import os
+import re
 import shlex
 import time
 
@@ -80,6 +81,7 @@
         if not self.enable_aslr:
           self.DisableASLR(machine, label.chromeos_root)
         self.PinGovernorExecutionFrequencies(machine, label.chromeos_root)
+        self.SetupCpuUsage(machine, label.chromeos_root)
         if benchmark.suite == 'telemetry_Crosperf':
           self.DecreaseWaitTime(machine, label.chromeos_root)
           ret_tup = self.Telemetry_Crosperf_Run(machine, label, benchmark,
@@ -173,6 +175,126 @@
     self.logger.LogFatalIf(
         ret, 'Could not pin frequencies on machine: %s' % machine_name)
 
+  def SetupCpuUsage(self, machine_name, chromeos_root):
+    """Setup CPU usage.
+
+    Based on self.dut_config['cpu_usage'] configure CPU cores
+    utilization.
+    """
+
+    if (self.dut_config['cpu_usage'] == 'big_only' or
+        self.dut_config['cpu_usage'] == 'little_only'):
+      ret, arch, _ = self._ce.CrosRunCommandWOutput(
+          'uname -m', machine=machine_name, chromeos_root=chromeos_root)
+      self.logger.LogFatalIf(
+          ret, 'Setup CPU failed. Could not retrieve architecture model.')
+
+      if arch.lower().startswith('arm') or arch.lower().startswith('aarch64'):
+        self.SetupArmCores(machine_name, chromeos_root)
+
+  def SetupArmCores(self, machine_name, chromeos_root):
+    """Setup ARM big/little cores."""
+
+    # CPU implemeters/part numbers of big/LITTLE CPU.
+    # Format: dict(CPU implementer: set(CPU part numbers))
+    LITTLE_CORES = {
+        '0x41': {
+            '0xd01',  # Cortex A32
+            '0xd03',  # Cortex A53
+            '0xd04',  # Cortex A35
+            '0xd05',  # Cortex A55
+        },
+    }
+    BIG_CORES = {
+        '0x41': {
+            '0xd07',  # Cortex A57
+            '0xd08',  # Cortex A72
+            '0xd09',  # Cortex A73
+            '0xd0a',  # Cortex A75
+            '0xd0b',  # Cortex A76
+        },
+    }
+
+    # Values of CPU Implementer and CPU part number are exposed by cpuinfo.
+    # Format:
+    # =================
+    # processor       : 0
+    # model name      : ARMv8 Processor rev 4 (v8l)
+    # BogoMIPS        : 48.00
+    # Features        : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4
+    # CPU implementer : 0x41
+    # CPU architecture: 8
+    # CPU variant     : 0x0
+    # CPU part        : 0xd03
+    # CPU revision    : 4
+
+    ret, cpuinfo, _ = self._ce.CrosRunCommandWOutput(
+        'cat /proc/cpuinfo', machine=machine_name, chromeos_root=chromeos_root)
+    self.logger.LogFatalIf(ret,
+                           'Setup ARM CPU failed. Could not retrieve cpuinfo.')
+
+    # List of all CPU cores: 0, 1, ..
+    proc_matches = re.findall(r'^processor\s*: (\d+)$', cpuinfo, re.MULTILINE)
+    # List of all corresponding CPU implementers
+    impl_matches = re.findall(r'^CPU implementer\s*: (0x[\da-f]+)$', cpuinfo,
+                              re.MULTILINE)
+    # List of all corresponding CPU part numbers
+    part_matches = re.findall(r'^CPU part\s*: (0x[\da-f]+)$', cpuinfo,
+                              re.MULTILINE)
+    assert len(proc_matches) == len(impl_matches)
+    assert len(part_matches) == len(impl_matches)
+
+    all_cores = set(proc_matches)
+    dut_big_cores = {
+        core
+        for core, impl, part in zip(proc_matches, impl_matches, part_matches)
+        if impl in BIG_CORES and part in BIG_CORES[impl]
+    }
+    dut_lit_cores = {
+        core
+        for core, impl, part in zip(proc_matches, impl_matches, part_matches)
+        if impl in LITTLE_CORES and part in LITTLE_CORES[impl]
+    }
+
+    if self.dut_config['cpu_usage'] == 'big_only':
+      cores_to_enable = dut_big_cores
+      cores_to_disable = all_cores - dut_big_cores
+    elif self.dut_config['cpu_usage'] == 'little_only':
+      cores_to_enable = dut_lit_cores
+      cores_to_disable = all_cores - dut_lit_cores
+    else:
+      self.logger.LogError(
+          'cpu_usage=%s is not supported on ARM.\n'
+          'Ignore ARM CPU setup and continue.' % self.dut_config['cpu_usage'])
+      return
+
+    if cores_to_enable:
+      cmd_enable_cores = ('echo 1 | tee /sys/devices/system/cpu/cpu{%s}/online'
+                          % ','.join(sorted(cores_to_enable)))
+
+      cmd_disable_cores = ''
+      if cores_to_disable:
+        cmd_disable_cores = (
+            'echo 0 | tee /sys/devices/system/cpu/cpu{%s}/online' % ','.join(
+                sorted(cores_to_disable)))
+
+      ret = self._ce.CrosRunCommand(
+          '; '.join([cmd_enable_cores, cmd_disable_cores]),
+          machine=machine_name,
+          chromeos_root=chromeos_root)
+      self.logger.LogFatalIf(
+          ret, 'Setup ARM CPU failed. Could not retrieve cpuinfo.')
+    else:
+      # If there are no cores enabled by dut_config then configuration
+      # is invalid for current platform and should be ignored.
+      self.logger.LogError(
+          '"cpu_usage" is invalid for targeted platform.\n'
+          'dut_config[cpu_usage]=%s\n'
+          'dut big cores: %s\n'
+          'dut little cores: %s\n'
+          'Ignore ARM CPU setup and continue.' % (self.dut_config['cpu_usage'],
+                                                  dut_big_cores, dut_lit_cores))
+
   def DecreaseWaitTime(self, machine_name, chromeos_root):
     """Change the ten seconds wait time for pagecycler to two seconds."""
     FILE = '/usr/local/telemetry/src/tools/perf/page_sets/page_cycler_story.py'
diff --git a/crosperf/suite_runner_unittest.py b/crosperf/suite_runner_unittest.py
index 4af6f4e..c5a4c18 100755
--- a/crosperf/suite_runner_unittest.py
+++ b/crosperf/suite_runner_unittest.py
@@ -12,8 +12,8 @@
 import os.path
 import time
 
-import mock
 import unittest
+import mock
 
 import suite_runner
 import label
@@ -23,6 +23,80 @@
 from cros_utils import command_executer
 from cros_utils import logger
 
+BIG_LITTLE_CPUINFO = """processor       : 0
+model name      : ARMv8 Processor rev 4 (v8l)
+BogoMIPS        : 48.00
+Features        : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4
+CPU implementer : 0x41
+CPU architecture: 8
+CPU variant     : 0x0
+CPU part        : 0xd03
+CPU revision    : 4
+
+processor       : 1
+model name      : ARMv8 Processor rev 4 (v8l)
+BogoMIPS        : 48.00
+Features        : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4
+CPU implementer : 0x41
+CPU architecture: 8
+CPU variant     : 0x0
+CPU part        : 0xd03
+CPU revision    : 4
+
+processor       : 2
+model name      : ARMv8 Processor rev 2 (v8l)
+BogoMIPS        : 48.00
+Features        : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4
+CPU implementer : 0x41
+CPU architecture: 8
+CPU variant     : 0x0
+CPU part        : 0xd08
+CPU revision    : 2
+"""
+LITTLE_ONLY_CPUINFO = """processor       : 0
+model name      : ARMv8 Processor rev 4 (v8l)
+BogoMIPS        : 48.00
+Features        : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4
+CPU implementer : 0x41
+CPU architecture: 8
+CPU variant     : 0x0
+CPU part        : 0xd03
+CPU revision    : 4
+
+processor       : 1
+model name      : ARMv8 Processor rev 4 (v8l)
+BogoMIPS        : 48.00
+Features        : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4
+CPU implementer : 0x41
+CPU architecture: 8
+CPU variant     : 0x0
+CPU part        : 0xd03
+CPU revision    : 4
+"""
+
+NOT_BIG_LITTLE_CPUINFO = """processor       : 0
+model name      : ARMv7 Processor rev 1 (v7l)
+Features        : swp half thumb fastmult vfp edsp thumbee neon vfpv3 tls vfpv4
+CPU implementer : 0x41
+CPU architecture: 7
+CPU variant     : 0x0
+CPU part        : 0xc0d
+CPU revision    : 1
+
+processor       : 1
+model name      : ARMv7 Processor rev 1 (v7l)
+Features        : swp half thumb fastmult vfp edsp thumbee neon vfpv3 tls vfpv4
+CPU implementer : 0x41
+CPU architecture: 7
+CPU variant     : 0x0
+CPU part        : 0xc0d
+CPU revision    : 1
+
+Hardware        : Rockchip (Device Tree)
+Revision        : 0000
+Serial          : 0000000000000000
+"""
+
 
 class SuiteRunnerTest(unittest.TestCase):
   """Class of SuiteRunner test."""
@@ -68,6 +142,7 @@
     self.call_test_that_run = False
     self.disable_aslr_args = []
     self.pin_governor_args = []
+    self.setup_cpu_usage_args = []
     self.skylab_run_args = []
     self.test_that_args = []
     self.telemetry_run_args = []
@@ -76,6 +151,7 @@
     self.call_telemetry_crosperf_run = False
     self.call_disable_aslr = False
     self.call_pin_governor = False
+    self.call_setup_cpu_usage = False
 
   def setUp(self):
     self.runner = suite_runner.SuiteRunner(
@@ -93,10 +169,12 @@
 
     def reset():
       self.call_pin_governor = False
+      self.call_setup_cpu_usage = False
       self.call_test_that_run = False
       self.call_skylab_run = False
       self.call_telemetry_crosperf_run = False
       self.pin_governor_args = []
+      self.setup_cpu_usage_args = []
       self.skylab_run_args = []
       self.test_that_args = []
       self.telemetry_run_args = []
@@ -110,6 +188,10 @@
       self.call_pin_governor = True
       self.pin_governor_args = [machine, chroot]
 
+    def FakeSetupCpuUsage(machine, chroot):
+      self.call_setup_cpu_usage = True
+      self.setup_cpu_usage_args = [machine, chroot]
+
     def FakeSkylabRun(test_label, benchmark, test_args, profiler_args):
       self.skylab_run_args = [test_label, benchmark, test_args, profiler_args]
       self.call_skylab_run = True
@@ -133,6 +215,7 @@
 
     self.runner.DisableASLR = FakeDisableASLR
     self.runner.PinGovernorExecutionFrequencies = FakePinGovernor
+    self.runner.SetupCpuUsage = FakeSetupCpuUsage
     self.runner.Skylab_Run = FakeSkylabRun
     self.runner.Telemetry_Crosperf_Run = FakeTelemetryCrosperfRun
     self.runner.Test_That_Run = FakeTestThatRun
@@ -146,6 +229,7 @@
                     profiler_args)
     self.assertFalse(self.call_disable_aslr)
     self.assertFalse(self.call_pin_governor)
+    self.assertFalse(self.call_setup_cpu_usage)
     self.assertTrue(self.call_skylab_run)
     self.assertFalse(self.call_test_that_run)
     self.assertFalse(self.call_telemetry_crosperf_run)
@@ -158,6 +242,9 @@
                     profiler_args)
     self.assertTrue(self.call_disable_aslr)
     self.assertTrue(self.call_pin_governor)
+    self.assertTrue(self.call_setup_cpu_usage)
+    self.assertEqual(self.setup_cpu_usage_args,
+                     [machine, self.mock_label.chromeos_root])
     self.assertTrue(self.call_test_that_run)
     self.assertFalse(self.call_telemetry_crosperf_run)
     self.assertEqual(
@@ -169,6 +256,7 @@
                     test_args, profiler_args)
     self.assertTrue(self.call_disable_aslr)
     self.assertTrue(self.call_pin_governor)
+    self.assertTrue(self.call_setup_cpu_usage)
     self.assertFalse(self.call_test_that_run)
     self.assertTrue(self.call_telemetry_crosperf_run)
     self.assertEqual(self.telemetry_crosperf_args, [
@@ -217,6 +305,105 @@
     # pyformat: enable
     self.assertEqual(cmd, (set_cpu_cmd,))
 
+  @mock.patch.object(suite_runner.SuiteRunner, 'SetupArmCores')
+  @mock.patch.object(command_executer.CommandExecuter, 'CrosRunCommandWOutput')
+  def test_setup_cpu_usage_little_on_arm(self, mock_cros_runcmd_wout,
+                                         mock_setup_arm):
+    self.mock_cmd_exec.CrosRunCommandWOutput = mock_cros_runcmd_wout
+    self.runner.SetupArmCores = mock_setup_arm
+    mock_cros_runcmd_wout.return_value = (0, 'armv7l', '')
+    self.runner.dut_config['cpu_usage'] = 'little_only'
+    self.runner.SetupCpuUsage('remote.cros', '/tmp/chromeos')
+    self.assertEqual(mock_setup_arm.call_count, 1)
+
+  @mock.patch.object(suite_runner.SuiteRunner, 'SetupArmCores')
+  @mock.patch.object(command_executer.CommandExecuter, 'CrosRunCommandWOutput')
+  def test_setup_cpu_usage_big_on_aarch64(self, mock_cros_runcmd_wout,
+                                          mock_setup_arm):
+    self.mock_cmd_exec.CrosRunCommandWOutput = mock_cros_runcmd_wout
+    self.runner.SetupArmCores = mock_setup_arm
+    mock_cros_runcmd_wout.return_value = (0, 'aarch64', '')
+    self.runner.dut_config['cpu_usage'] = 'big_only'
+    self.runner.SetupCpuUsage('remote.cros', '/tmp/chromeos')
+    self.assertEqual(mock_setup_arm.call_count, 1)
+
+  @mock.patch.object(suite_runner.SuiteRunner, 'SetupArmCores')
+  @mock.patch.object(command_executer.CommandExecuter, 'CrosRunCommandWOutput')
+  def test_setup_cpu_usage_all_on_intel(self, mock_cros_runcmd_wout,
+                                        mock_setup_arm):
+    self.mock_cmd_exec.CrosRunCommandWOutput = mock_cros_runcmd_wout
+    self.runner.SetupArmCores = mock_setup_arm
+    mock_cros_runcmd_wout.return_value = (0, 'x86_64', '')
+    self.runner.dut_config['cpu_usage'] = 'all'
+    self.runner.SetupCpuUsage('remote.cros', '/tmp/chromeos')
+    # Check that SetupArmCores not called.
+    self.assertEqual(mock_setup_arm.call_count, 0)
+
+  @mock.patch.object(command_executer.CommandExecuter, 'CrosRunCommandWOutput')
+  @mock.patch.object(command_executer.CommandExecuter, 'CrosRunCommand')
+  def test_setup_arm_cores_big_on_big_little(self, mock_cros_runcmd,
+                                             mock_cros_runcmd_wout):
+    self.mock_cmd_exec.CrosRunCommand = mock_cros_runcmd
+    self.mock_cmd_exec.CrosRunCommandWOutput = mock_cros_runcmd_wout
+    mock_cros_runcmd_wout.return_value = (0, BIG_LITTLE_CPUINFO, '')
+    self.runner.dut_config['cpu_usage'] = 'big_only'
+    self.runner.SetupArmCores('remote.cros', '/tmp/chromeos')
+    self.assertEqual(mock_cros_runcmd.call_args_list[0][0],
+                     ('echo 1 | tee /sys/devices/system/cpu/cpu{2}/online; '
+                      'echo 0 | tee /sys/devices/system/cpu/cpu{0,1}/online',))
+
+  @mock.patch.object(command_executer.CommandExecuter, 'CrosRunCommandWOutput')
+  @mock.patch.object(command_executer.CommandExecuter, 'CrosRunCommand')
+  def test_setup_arm_cores_little_on_big_little(self, mock_cros_runcmd,
+                                                mock_cros_runcmd_wout):
+    self.mock_cmd_exec.CrosRunCommand = mock_cros_runcmd
+    self.mock_cmd_exec.CrosRunCommandWOutput = mock_cros_runcmd_wout
+    mock_cros_runcmd_wout.return_value = (0, BIG_LITTLE_CPUINFO, '')
+    self.runner.dut_config['cpu_usage'] = 'little_only'
+    self.runner.SetupArmCores('remote.cros', '/tmp/chromeos')
+    self.assertEqual(mock_cros_runcmd.call_args_list[0][0],
+                     ('echo 1 | tee /sys/devices/system/cpu/cpu{0,1}/online; '
+                      'echo 0 | tee /sys/devices/system/cpu/cpu{2}/online',))
+
+  @mock.patch.object(command_executer.CommandExecuter, 'CrosRunCommandWOutput')
+  @mock.patch.object(command_executer.CommandExecuter, 'CrosRunCommand')
+  def test_setup_arm_cores_invalid_config(self, mock_cros_runcmd,
+                                          mock_cros_runcmd_wout):
+    self.mock_cmd_exec.CrosRunCommand = mock_cros_runcmd
+    self.mock_cmd_exec.CrosRunCommandWOutput = mock_cros_runcmd_wout
+    mock_cros_runcmd_wout.return_value = (0, LITTLE_ONLY_CPUINFO, '')
+    self.runner.dut_config['cpu_usage'] = 'big_only'
+    self.runner.SetupArmCores('remote.cros', '/tmp/chromeos')
+    # Check that CrosRun is not called when trying
+    # to use 'big_only' on a platform with all little cores.
+    self.assertEqual(mock_cros_runcmd.call_count, 0)
+
+  @mock.patch.object(command_executer.CommandExecuter, 'CrosRunCommandWOutput')
+  @mock.patch.object(command_executer.CommandExecuter, 'CrosRunCommand')
+  def test_setup_arm_cores_not_big_little(self, mock_cros_runcmd,
+                                          mock_cros_runcmd_wout):
+    self.mock_cmd_exec.CrosRunCommand = mock_cros_runcmd
+    self.mock_cmd_exec.CrosRunCommandWOutput = mock_cros_runcmd_wout
+    mock_cros_runcmd_wout.return_value = (0, NOT_BIG_LITTLE_CPUINFO, '')
+    self.runner.dut_config['cpu_usage'] = 'big_only'
+    self.runner.SetupArmCores('remote.cros', '/tmp/chromeos')
+    # Check that CrosRun is not called when trying
+    # to use 'big_only' on a platform w/o support of big/little.
+    self.assertEqual(mock_cros_runcmd.call_count, 0)
+
+  @mock.patch.object(command_executer.CommandExecuter, 'CrosRunCommandWOutput')
+  @mock.patch.object(command_executer.CommandExecuter, 'CrosRunCommand')
+  def test_setup_arm_cores_unsupported_cpu_usage(self, mock_cros_runcmd,
+                                                 mock_cros_runcmd_wout):
+    self.mock_cmd_exec.CrosRunCommand = mock_cros_runcmd
+    self.mock_cmd_exec.CrosRunCommandWOutput = mock_cros_runcmd_wout
+    mock_cros_runcmd_wout.return_value = (0, BIG_LITTLE_CPUINFO, '')
+    self.runner.dut_config['cpu_usage'] = 'exclusive_cores'
+    self.runner.SetupArmCores('remote.cros', '/tmp/chromeos')
+    # Check that CrosRun is not called when trying to use
+    # 'exclusive_cores' on ARM CPU setup.
+    self.assertEqual(mock_cros_runcmd.call_count, 0)
+
   @mock.patch.object(command_executer.CommandExecuter, 'CrosRunCommand')
   def test_reboot_machine(self, mock_cros_runcmd):