tests: cpus awareness

Introduce MAX_CPUS parameters to control maximum number of CPUs used by
VPP(s) during testing, with default value 'auto' corresponding to all
CPUs available.

Calculate test CPU requirements by taking into account the number of
workers, so a test requires 1 (main thread) + # of worker CPUs.

When running tests, keep track of both running test jobs (controlled by
TEST_JOBS parameter) and free CPUs. This then causes two limits in the
system - to not exceed number of jobs in parallel but also to not exceed
number of CPUs available.

Skip tests which require more CPUs than are available in system (or more
than MAX_CPUS) and print a warning message.

Type: improvement
Change-Id: Ib8fda54e4c6a36179d64160bb87fbd3a0011762d
Signed-off-by: Klement Sekera <ksekera@cisco.com>
This commit is contained in:
Klement Sekera
2021-04-08 19:37:41 +02:00
committed by Andrew Yourtchenko
parent f70cf23376
commit 558ceabc6c
8 changed files with 294 additions and 158 deletions

View File

@ -16,11 +16,24 @@ from vpp_papi import VppEnum
@tag_run_solo
class TestMemif(VppTestCase):
""" Memif Test Case """
remote_class = RemoteVppTestCase
@classmethod
def get_cpus_required(cls):
return (super().get_cpus_required() +
cls.remote_class.get_cpus_required())
@classmethod
def assign_cpus(cls, cpus):
remote_cpus = cpus[:cls.remote_class.get_cpus_required()]
my_cpus = cpus[cls.remote_class.get_cpus_required():]
cls.remote_class.assign_cpus(remote_cpus)
super().assign_cpus(my_cpus)
@classmethod
def setUpClass(cls):
# fork new process before client connects to VPP
cls.remote_test = RemoteClass(RemoteVppTestCase)
cls.remote_test = RemoteClass(cls.remote_class)
cls.remote_test.start_remote()
cls.remote_test.set_request_timeout(10)
super(TestMemif, cls).setUpClass()

View File

@ -366,7 +366,8 @@ help:
@echo "Arguments controlling test runs:"
@echo " V=[0|1|2] - set test verbosity level"
@echo " 0=ERROR, 1=INFO, 2=DEBUG"
@echo " TEST_JOBS=[<n>|auto] - use <n> parallel processes for test execution or automatic discovery of maximum acceptable processes (default: 1)"
@echo " TEST_JOBS=[<n>|auto] - use at most <n> parallel python processes for test execution, if auto, set to number of available cpus (default: 1)"
@echo " MAX_VPP_CPUS=[<n>|auto]- use at most <n> cpus for running vpp main and worker threads, if auto, set to number of available cpus (default: auto)"
@echo " CACHE_OUTPUT=[0|1] - cache VPP stdout/stderr and log as one block after test finishes (default: 1)"
@echo " FAILFAST=[0|1] - fail fast if 1, complete all tests if 0"
@echo " TIMEOUT=<timeout> - fail test suite if any single test takes longer than <timeout> (in seconds) to finish (default: 600)"

21
test/cpu_config.py Normal file
View File

@ -0,0 +1,21 @@
import os
import psutil
available_cpus = psutil.Process().cpu_affinity()
num_cpus = len(available_cpus)
max_vpp_cpus = os.getenv("MAX_VPP_CPUS", "auto").lower()
if max_vpp_cpus == "auto":
max_vpp_cpus = num_cpus
else:
try:
max_vpp_cpus = int(max_vpp_cpus)
except ValueError as e:
raise ValueError("Invalid MAX_VPP_CPUS value specified, valid "
"values are a positive integer or 'auto'") from e
if max_vpp_cpus <= 0:
raise ValueError("Invalid MAX_VPP_CPUS value specified, valid "
"values are a positive integer or 'auto'")
if max_vpp_cpus > num_cpus:
max_vpp_cpus = num_cpus

View File

@ -22,6 +22,7 @@ from inspect import getdoc, isclass
from traceback import format_exception
from logging import FileHandler, DEBUG, Formatter
from enum import Enum
from abc import ABC, abstractmethod
import scapy.compat
from scapy.packet import Raw
@ -42,6 +43,8 @@ from scapy.layers.inet import IPerror, TCPerror, UDPerror, ICMPerror
from scapy.layers.inet6 import ICMPv6DestUnreach, ICMPv6EchoRequest
from scapy.layers.inet6 import ICMPv6EchoReply
from cpu_config import available_cpus, num_cpus, max_vpp_cpus
logger = logging.getLogger(__name__)
# Set up an empty logger for the testcase that can be overridden as necessary
@ -53,6 +56,7 @@ FAIL = 1
ERROR = 2
SKIP = 3
TEST_RUN = 4
SKIP_CPU_SHORTAGE = 5
class BoolEnvironmentVariable(object):
@ -223,6 +227,21 @@ def _running_gcov_tests():
running_gcov_tests = _running_gcov_tests()
def get_environ_vpp_worker_count():
worker_config = os.getenv("VPP_WORKER_CONFIG", None)
if worker_config:
elems = worker_config.split(" ")
if elems[0] != "workers" or len(elems) != 2:
raise ValueError("Wrong VPP_WORKER_CONFIG == '%s' value." %
worker_config)
return int(elems[1])
else:
return 0
environ_vpp_worker_count = get_environ_vpp_worker_count()
class KeepAliveReporter(object):
"""
Singleton object which reports test start to parent process
@ -292,7 +311,21 @@ class DummyVpp:
pass
class VppTestCase(unittest.TestCase):
class CPUInterface(ABC):
cpus = []
skipped_due_to_cpu_lack = False
@classmethod
@abstractmethod
def get_cpus_required(cls):
pass
@classmethod
def assign_cpus(cls, cpus):
cls.cpus = cpus
class VppTestCase(CPUInterface, unittest.TestCase):
"""This subclass is a base class for VPP test cases that are implemented as
classes. It provides methods to create and run test case.
"""
@ -358,34 +391,18 @@ class VppTestCase(unittest.TestCase):
if dl == "gdb-all" or dl == "gdbserver-all":
cls.debug_all = True
@staticmethod
def get_least_used_cpu():
cpu_usage_list = [set(range(psutil.cpu_count()))]
vpp_processes = [p for p in psutil.process_iter(attrs=['pid', 'name'])
if 'vpp_main' == p.info['name']]
for vpp_process in vpp_processes:
for cpu_usage_set in cpu_usage_list:
try:
cpu_num = vpp_process.cpu_num()
if cpu_num in cpu_usage_set:
cpu_usage_set_index = cpu_usage_list.index(
cpu_usage_set)
if cpu_usage_set_index == len(cpu_usage_list) - 1:
cpu_usage_list.append({cpu_num})
@classmethod
def get_vpp_worker_count(cls):
if not hasattr(cls, "vpp_worker_count"):
if cls.has_tag(TestCaseTag.FIXME_VPP_WORKERS):
cls.vpp_worker_count = 0
else:
cpu_usage_list[cpu_usage_set_index + 1].add(
cpu_num)
cpu_usage_set.remove(cpu_num)
break
except psutil.NoSuchProcess:
pass
cls.vpp_worker_count = environ_vpp_worker_count
return cls.vpp_worker_count
for cpu_usage_set in cpu_usage_list:
if len(cpu_usage_set) > 0:
min_usage_set = cpu_usage_set
break
return random.choice(tuple(min_usage_set))
@classmethod
def get_cpus_required(cls):
return 1 + cls.get_vpp_worker_count()
@classmethod
def setUpConstants(cls):
@ -417,20 +434,6 @@ class VppTestCase(unittest.TestCase):
if coredump_size is None:
coredump_size = "coredump-size unlimited"
cpu_core_number = cls.get_least_used_cpu()
if not hasattr(cls, "vpp_worker_count"):
cls.vpp_worker_count = 0
worker_config = os.getenv("VPP_WORKER_CONFIG", "")
if worker_config:
elems = worker_config.split(" ")
if elems[0] != "workers" or len(elems) != 2:
raise ValueError("Wrong VPP_WORKER_CONFIG == '%s' value." %
worker_config)
cls.vpp_worker_count = int(elems[1])
if cls.vpp_worker_count > 0 and\
cls.has_tag(TestCaseTag.FIXME_VPP_WORKERS):
cls.vpp_worker_count = 0
default_variant = os.getenv("VARIANT")
if default_variant is not None:
default_variant = "defaults { %s 100 }" % default_variant
@ -447,9 +450,10 @@ class VppTestCase(unittest.TestCase):
coredump_size, "runtime-dir", cls.tempdir, "}",
"api-trace", "{", "on", "}",
"api-segment", "{", "prefix", cls.get_api_segment_prefix(), "}",
"cpu", "{", "main-core", str(cpu_core_number), ]
if cls.vpp_worker_count:
cls.vpp_cmdline.extend(["workers", str(cls.vpp_worker_count)])
"cpu", "{", "main-core", str(cls.cpus[0]), ]
if cls.get_vpp_worker_count():
cls.vpp_cmdline.extend([
"corelist-workers", ",".join([str(x) for x in cls.cpus[1:]])])
cls.vpp_cmdline.extend([
"}",
"physmem", "{", "max-size", "32m", "}",
@ -509,11 +513,12 @@ class VppTestCase(unittest.TestCase):
@classmethod
def run_vpp(cls):
cls.logger.debug(f"Assigned cpus: {cls.cpus}")
cmdline = cls.vpp_cmdline
if cls.debug_gdbserver:
gdbserver = '/usr/bin/gdbserver'
if not os.path.isfile(gdbserver) or \
if not os.path.isfile(gdbserver) or\
not os.access(gdbserver, os.X_OK):
raise Exception("gdbserver binary '%s' does not exist or is "
"not executable" % gdbserver)
@ -1349,6 +1354,7 @@ class VppTestResult(unittest.TestResult):
self.verbosity = verbosity
self.result_string = None
self.runner = runner
self.printed = []
def addSuccess(self, test):
"""
@ -1383,6 +1389,9 @@ class VppTestResult(unittest.TestResult):
unittest.TestResult.addSkip(self, test, reason)
self.result_string = colorize("SKIP", YELLOW)
if reason == "not enough cpus":
self.send_result_through_pipe(test, SKIP_CPU_SHORTAGE)
else:
self.send_result_through_pipe(test, SKIP)
def symlink_failed(self):
@ -1501,28 +1510,34 @@ class VppTestResult(unittest.TestResult):
"""
def print_header(test):
if test.__class__ in self.printed:
return
test_doc = getdoc(test)
if not test_doc:
raise Exception("No doc string for test '%s'" % test.id())
test_title = test_doc.splitlines()[0]
test_title_colored = colorize(test_title, GREEN)
test_title = colorize(test_title, GREEN)
if test.is_tagged_run_solo():
# long live PEP-8 and 80 char width limitation...
c = YELLOW
test_title_colored = colorize("SOLO RUN: " + test_title, c)
test_title = colorize(f"SOLO RUN: {test_title}", YELLOW)
# This block may overwrite the colorized title above,
# but we want this to stand out and be fixed
if test.has_tag(TestCaseTag.FIXME_VPP_WORKERS):
c = RED
w = "FIXME with VPP workers: "
test_title_colored = colorize(w + test_title, c)
test_title = colorize(
f"FIXME with VPP workers: {test_title}", RED)
if test.__class__.skipped_due_to_cpu_lack:
test_title = colorize(
f"{test_title} [skipped - not enough cpus, "
f"required={test.__class__.get_cpus_required()}, "
f"available={max_vpp_cpus}]", YELLOW)
if not hasattr(test.__class__, '_header_printed'):
print(double_line_delim)
print(test_title_colored)
print(test_title)
print(double_line_delim)
test.__class__._header_printed = True
self.printed.append(test.__class__)
print_header(test)
self.start_test = time.time()

View File

@ -11,7 +11,7 @@ single_line_delim = '-' * 78
def colorize(msg, color):
return color + msg + COLOR_RESET
return f"{color}{msg}{COLOR_RESET}"
class ColorFormatter(logging.Formatter):

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,7 @@ from framework import VppDiedError, VppTestCase, KeepAliveReporter
class SanityTestCase(VppTestCase):
""" Sanity test case - verify whether VPP is able to start """
pass
cpus = [0]
# don't ask to debug SanityTestCase
@classmethod

View File

@ -2,11 +2,11 @@
"""Test framework utility functions tests"""
import unittest
from framework import VppTestRunner
from framework import VppTestRunner, CPUInterface
from vpp_papi import mac_pton, mac_ntop
class TestUtil (unittest.TestCase):
class TestUtil (CPUInterface, unittest.TestCase):
""" Test framework utility tests """
@classmethod
@ -23,6 +23,10 @@ class TestUtil (unittest.TestCase):
pass
return False
@classmethod
def get_cpus_required(cls):
return 0
def test_mac_to_binary(self):
""" MAC to binary and back """
mac = 'aa:bb:cc:dd:ee:ff'