Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 62 additions & 5 deletions lisa/tools/iperf3.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import re
import time
from decimal import Decimal
from typing import TYPE_CHECKING, Any, Dict, List, Pattern, Type, cast
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Pattern, Type, cast

from retry import retry

Expand All @@ -19,7 +19,14 @@
)
from lisa.operating_system import Posix
from lisa.tools import Cat
from lisa.util import LisaException, check_till_timeout, constants, get_matched_str
from lisa.util import (
LisaException,
LisaVersionInfo,
check_till_timeout,
constants,
get_matched_str,
parse_version,
)
from lisa.util.perf_timer import create_timer
from lisa.util.process import ExecutableResult, Process

Expand Down Expand Up @@ -69,7 +76,20 @@

class Iperf3(Tool):
_repo = "https://github.com/esnet/iperf"
_branch = "3.10.1"
# iperf 3.16 introduced a multi-threaded (thread-per-stream) model that
# regressed UDP mode: with high stream counts (e.g. -P 64) the client
# segfaults instead of emitting the JSON report the perf tests parse,
# which makes the tests fail (Ubuntu 24.04 ships the affected 3.16). Those
# threading crashes were fixed upstream in 3.18, so only 3.16-3.17 are
# affected. Such a version is replaced by building a vetted, fixed release
# (``_branch``) from source, which keeps the multi-threaded throughput
# gains. Older single-threaded versions (< 3.16) and already-fixed ones
# (>= 3.18) are kept as-is, so a source build only happens where needed.
_first_multithreaded_version = "3.16.0"
_first_fixed_version = "3.18.0"
_branch = "3.21"
# Matches the version in ``iperf 3.16 (cJSON 1.7.15)``.
_version_pattern = re.compile(r"iperf\s+(\d+\.\d+(?:\.\d+)?)")
_sender_pattern = re.compile(
r"(([\w\W]*?)[SUM].* (?P<bandwidth>[0-9]+.[0-9]+)"
r" Gbits/sec.*sender([\w\W]*?))",
Expand Down Expand Up @@ -100,6 +120,32 @@ def dependencies(self) -> List[Type[Tool]]:
def help(self) -> ExecutableResult:
return self.run("-h", force_run=True)

def get_version(self) -> Optional[LisaVersionInfo]:
# ``iperf3 --version`` prints e.g. ``iperf 3.16 (cJSON 1.7.15)``.
output = self.run("--version", force_run=True).stdout
version_string = get_matched_str(output, self._version_pattern)
if not version_string:
return None
return parse_version(version_string)

def _is_buggy_multithreaded_version(self) -> bool:
version = self.get_version()
if version is None:
# Could not determine the version; rebuild from source to be safe.
self._log.debug(
"Could not parse installed iperf3 version; "
"installing a fixed build from source."
)
return True
# Only 3.16-3.17 have the UDP threading segfault; 3.18+ fixed it
# upstream, so a distro shipping a fixed release keeps its package and
# no source build is triggered.
return bool(
parse_version(self._first_multithreaded_version)
<= version
< parse_version(self._first_fixed_version)
)

def install(self) -> bool:
posix_os: Posix = cast(Posix, self.node.os)
try:
Expand All @@ -110,6 +156,10 @@ def install(self) -> bool:
if self._check_exists():
if "--logfile" not in self.help().stdout:
install_from_src = True
elif self._is_buggy_multithreaded_version():
# The packaged iperf3 has the UDP multi-threading regression;
# replace it with a known-good release built from source.
install_from_src = True
else:
install_from_src = True
if install_from_src:
Expand Down Expand Up @@ -589,7 +639,9 @@ def _initialize(self, *args: Any, **kwargs: Any) -> None:
def _install_from_src(self) -> None:
tool_path = self.get_tool_path()
git = self.node.tools[Git]
git.clone(self._repo, tool_path)
# Pin to a fixed upstream release so the built binary does not have the
# UDP multi-threading segfault regression.
git.clone(self._repo, tool_path, ref=self._branch)
code_path = tool_path.joinpath("iperf")
make = self.node.tools[Make]
self.node.execute("./configure", cwd=code_path).assert_exit_code()
Expand All @@ -609,5 +661,10 @@ def _get_bandwidth(self, result: str, pattern: Pattern[str]) -> Decimal:
def _pre_handle(self, result: str) -> str:
result = result.replace("-nan", "0")
result_matched = get_matched_str(result, self._json_pattern)
assert result_matched, "fail to find json format results"
if not result_matched:
raise LisaException(
"failed to find JSON in iperf3 output; the client may have "
"crashed (e.g. segmentation fault) instead of reporting "
f"results: {result}"
)
return result_matched
Loading