aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xbootstrap-verify-perf.sh1
-rw-r--r--docs/tag_documentation.rst16
-rw-r--r--resources/libraries/python/IPUtil.py52
-rw-r--r--resources/libraries/python/TrafficGenerator.py18
-rw-r--r--resources/libraries/python/VppConfigGenerator.py92
-rw-r--r--resources/libraries/python/constants.py2
-rw-r--r--resources/libraries/python/tcp.py36
-rw-r--r--resources/libraries/python/topology.py4
-rw-r--r--resources/libraries/robot/performance/performance_setup.robot54
-rw-r--r--resources/libraries/robot/tcp/tcp_setup.robot43
-rw-r--r--resources/libraries/robot/wrk/wrk_utils.robot78
-rw-r--r--resources/templates/vat/start_http_server.vat1
-rw-r--r--resources/tools/__init__.py16
-rw-r--r--resources/tools/wrk/__init__.py16
-rw-r--r--resources/tools/wrk/doc/wrk_lld.rst293
-rw-r--r--resources/tools/wrk/wrk.py291
-rw-r--r--resources/tools/wrk/wrk_errors.py55
-rw-r--r--resources/tools/wrk/wrk_traffic_profile_parser.py286
-rwxr-xr-xresources/tools/wrk/wrk_utils.sh290
-rw-r--r--resources/traffic_profiles/wrk/wrk-sf-2n-ethip4tcphttp-1u1c1con-cps.yaml47
-rw-r--r--resources/traffic_profiles/wrk/wrk-sf-2n-ethip4tcphttp-1u1c50con-bw.yaml47
-rw-r--r--resources/traffic_profiles/wrk/wrk-sf-2n-ethip4tcphttp-1u1c50con-rps.yaml47
-rw-r--r--tests/vpp/perf/tcp/10ge2p1x520-ethip4tcphttp-httpserver.robot127
23 files changed, 1905 insertions, 7 deletions
diff --git a/bootstrap-verify-perf.sh b/bootstrap-verify-perf.sh
index 4b86c6ea37..e2d37e2d8c 100755
--- a/bootstrap-verify-perf.sh
+++ b/bootstrap-verify-perf.sh
@@ -244,7 +244,6 @@ case "$TEST_TAG" in
--include pdrdiscANDnic_intel-xl710AND1t1cANDipsechwANDbase \
--include pdrdiscANDnic_intel-xl710AND2t2cANDipsechwANDbase \
tests/
- RETURN_STATUS=$(echo $?)
;;
VPP-VERIFY-PERF-IP4 )
pybot ${PYBOT_ARGS} \
diff --git a/docs/tag_documentation.rst b/docs/tag_documentation.rst
index c6064c61a3..165feeec0a 100644
--- a/docs/tag_documentation.rst
+++ b/docs/tag_documentation.rst
@@ -235,6 +235,22 @@ Test type tags
Functional test cases for TLDK.
+.. topic:: TCP
+
+ Tests which use TCP.
+
+.. topic:: TCP_CPS
+
+ Performance tests which measure connections per second using http requests.
+
+.. topic:: TCP_RPS
+
+ Performance tests which measure requests per second using http requests.
+
+.. topic:: HTTP
+
+ Tests which use HTTP.
+
Forwarding mode tags
--------------------
diff --git a/resources/libraries/python/IPUtil.py b/resources/libraries/python/IPUtil.py
index d2f2adcf28..e215b301b9 100644
--- a/resources/libraries/python/IPUtil.py
+++ b/resources/libraries/python/IPUtil.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2016 Cisco and/or its affiliates.
+# Copyright (c) 2018 Cisco and/or its affiliates.
# 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:
@@ -13,6 +13,8 @@
"""Common IP utilities library."""
+import re
+
from ipaddress import IPv4Network, ip_address
from resources.libraries.python.ssh import SSH
@@ -138,6 +140,54 @@ class IPUtil(object):
exec_cmd_no_error(node, cmd, sudo=True)
@staticmethod
+ def get_linux_interface_name(node, pci_addr):
+ """Get the interface name.
+
+ :param node: Node where to execute command.
+ :param pci_addr: PCI address
+ :type node: dict
+ :type pci_addr: str
+ :returns: Interface name
+ :rtype: str
+ :raises RuntimeError: If cannot get the information about interfaces.
+ """
+
+ regex_intf_info = r"pci@" \
+ r"([0-9a-f]{4}:[0-9a-f]{2}:[0-9a-f]{2}.[0-9a-f])\s*" \
+ r"([a-zA-Z0-9]*)\s*network"
+
+ cmd = "lshw -class network -businfo"
+ ret_code, stdout, stderr = exec_cmd(node, cmd, timeout=30, sudo=True)
+ if ret_code != 0:
+ raise RuntimeError('Could not get information about interfaces, '
+ 'reason:{0}'.format(stderr))
+
+ for line in stdout.splitlines()[2:]:
+ try:
+ if re.search(regex_intf_info, line).group(1) == pci_addr:
+ return re.search(regex_intf_info, line).group(2)
+ except AttributeError:
+ continue
+ return None
+
+ @staticmethod
+ def set_linux_interface_up(node, interface):
+ """Set the specified interface up.
+
+ :param node: Node where to execute command.
+ :param interface: Interface in namespace.
+ :type node: dict
+ :type interface: str
+ :raises RuntimeError: If the interface could not be set up.
+ """
+
+ cmd = "ip link set {0} up".format(interface)
+ ret_code, _, stderr = exec_cmd(node, cmd, timeout=30, sudo=True)
+ if ret_code != 0:
+ raise RuntimeError('Could not set the interface up, reason:{0}'.
+ format(stderr))
+
+ @staticmethod
def set_linux_interface_ip(node, interface, ip_addr, prefix,
namespace=None):
"""Set IP address to interface in linux.
diff --git a/resources/libraries/python/TrafficGenerator.py b/resources/libraries/python/TrafficGenerator.py
index 698b67ead2..f363fe3f55 100644
--- a/resources/libraries/python/TrafficGenerator.py
+++ b/resources/libraries/python/TrafficGenerator.py
@@ -308,6 +308,24 @@ class TrafficGenerator(object):
raise RuntimeError('t-rex-64 startup failed')
@staticmethod
+ def is_trex_running(node):
+ """Check if TRex is running using pidof.
+
+ :param node: Traffic generator node.
+ :type node: dict
+ :returns: True if TRex is running otherwise False.
+ :rtype: bool
+ :raises: RuntimeError if node type is not a TG.
+ """
+ if node['type'] != NodeType.TG:
+ raise RuntimeError('Node type is not a TG')
+
+ ssh = SSH()
+ ssh.connect(node)
+ ret, _, _ = ssh.exec_command_sudo("pidof t-rex")
+ return bool(int(ret) == 0)
+
+ @staticmethod
def teardown_traffic_generator(node):
"""TG teardown.
diff --git a/resources/libraries/python/VppConfigGenerator.py b/resources/libraries/python/VppConfigGenerator.py
index c1bde49742..d4fde0a6c8 100644
--- a/resources/libraries/python/VppConfigGenerator.py
+++ b/resources/libraries/python/VppConfigGenerator.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2016 Cisco and/or its affiliates.
+# Copyright (c) 2018 Cisco and/or its affiliates.
# 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:
@@ -382,6 +382,96 @@ class VppConfigGenerator(object):
path = ['nat']
self.add_config_item(self._nodeconfig, value, path)
+ def add_tcp_preallocated_connections(self, value):
+ """Add TCP pre-allocated connections.
+
+ :param value: The number of pre-allocated connections.
+ :type value: int
+ """
+ path = ['tcp', 'preallocated-connections']
+ self.add_config_item(self._nodeconfig, value, path)
+
+ def add_tcp_preallocated_half_open_connections(self, value):
+ """Add TCP pre-allocated half open connections.
+
+ :param value: The number of pre-allocated half open connections.
+ :type value: int
+ """
+ path = ['tcp', 'preallocated-half-open-connections']
+ self.add_config_item(self._nodeconfig, value, path)
+
+ def add_session_event_queue_length(self, value):
+ """Add session event queue length.
+
+ :param value: Session event queue length.
+ :type value: int
+ """
+ path = ['session', 'event-queue-length']
+ self.add_config_item(self._nodeconfig, value, path)
+
+ def add_session_preallocated_sessions(self, value):
+ """Add the number of pre-allocated sessions.
+
+ :param value: Number of pre-allocated sessions.
+ :type value: int
+ """
+ path = ['session', 'preallocated-sessions']
+ self.add_config_item(self._nodeconfig, value, path)
+
+ def add_session_v4_session_table_buckets(self, value):
+ """Add number of v4 session table buckets to the config.
+
+ :param value: Number of v4 session table buckets.
+ :type value: int
+ """
+ path = ['session', 'v4-session-table-buckets']
+ self.add_config_item(self._nodeconfig, value, path)
+
+ def add_session_v4_session_table_memory(self, value):
+ """Add the size of v4 session table memory.
+
+ :param value: Size of v4 session table memory.
+ :type value: str
+ """
+ path = ['session', 'v4-session-table-memory']
+ self.add_config_item(self._nodeconfig, value, path)
+
+ def add_session_v4_halfopen_table_buckets(self, value):
+ """Add the number of v4 halfopen table buckets.
+
+ :param value: Number of v4 halfopen table buckets.
+ :type value: int
+ """
+ path = ['session', 'v4-halfopen-table-buckets']
+ self.add_config_item(self._nodeconfig, value, path)
+
+ def add_session_v4_halfopen_table_memory(self, value):
+ """Add the size of v4 halfopen table memory.
+
+ :param value: Size of v4 halfopen table memory.
+ :type value: str
+ """
+ path = ['session', 'v4-halfopen-table-memory']
+ self.add_config_item(self._nodeconfig, value, path)
+
+ def add_session_local_endpoints_table_buckets(self, value):
+ """Add the number of local endpoints table buckets.
+
+ :param value: Number of local endpoints table buckets.
+ :type value: int
+ """
+ path = ['session', 'local-endpoints-table-buckets']
+ self.add_config_item(self._nodeconfig, value, path)
+
+ def add_session_local_endpoints_table_memory(self, value):
+ """Add the size of local endpoints table memory.
+
+ :param value: Size of local endpoints table memory.
+ :type value: str
+ """
+ path = ['session', 'local-endpoints-table-memory']
+ self.add_config_item(self._nodeconfig, value, path)
+
def apply_config(self, filename=None, waittime=5,
retries=12, restart_vpp=True):
"""Generate and apply VPP configuration for node.
diff --git a/resources/libraries/python/constants.py b/resources/libraries/python/constants.py
index 30f7531947..c3f1551c55 100644
--- a/resources/libraries/python/constants.py
+++ b/resources/libraries/python/constants.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2016 Cisco and/or its affiliates.
+# Copyright (c) 2018 Cisco and/or its affiliates.
# 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:
diff --git a/resources/libraries/python/tcp.py b/resources/libraries/python/tcp.py
new file mode 100644
index 0000000000..5ae1ebf929
--- /dev/null
+++ b/resources/libraries/python/tcp.py
@@ -0,0 +1,36 @@
+# Copyright (c) 2018 Cisco and/or its affiliates.
+# 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.
+
+"""TCP util library.
+"""
+
+from resources.libraries.python.VatExecutor import VatTerminal
+
+
+class TCPUtils(object):
+ """Implementation of the TCP utilities.
+ """
+
+ def __init__(self):
+ pass
+
+ @staticmethod
+ def start_http_server(node):
+ """Start HTTP server on the given node.
+
+ :param node: Node to start HTTP server on.
+ :type node: dict
+ """
+
+ with VatTerminal(node) as vat:
+ vat.vat_terminal_exec_cmd_from_template("start_http_server.vat")
diff --git a/resources/libraries/python/topology.py b/resources/libraries/python/topology.py
index 94652dbd23..38e08d23d3 100644
--- a/resources/libraries/python/topology.py
+++ b/resources/libraries/python/topology.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2016 Cisco and/or its affiliates.
+# Copyright (c) 2018 Cisco and/or its affiliates.
# 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:
@@ -21,7 +21,7 @@ from robot.api import logger
from robot.libraries.BuiltIn import BuiltIn, RobotNotRunningError
from robot.api.deco import keyword
-__all__ = ["DICT__nodes", 'Topology']
+__all__ = ["DICT__nodes", 'Topology', 'NodeType']
def load_topo_from_yaml():
diff --git a/resources/libraries/robot/performance/performance_setup.robot b/resources/libraries/robot/performance/performance_setup.robot
index 0453fad275..0e909aa455 100644
--- a/resources/libraries/robot/performance/performance_setup.robot
+++ b/resources/libraries/robot/performance/performance_setup.robot
@@ -1,4 +1,4 @@
-# Copyright (c) 2017 Cisco and/or its affiliates.
+# Copyright (c) 2018 Cisco and/or its affiliates.
# 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:
@@ -13,6 +13,7 @@
*** Settings ***
| Library | resources.libraries.python.DUTSetup
+| Library | resources.tools.wrk.wrk
| Resource | resources/libraries/robot/performance/performance_configuration.robot
| Resource | resources/libraries/robot/performance/performance_utils.robot
| Documentation | Performance suite keywords - Suite and test setups and
@@ -385,6 +386,45 @@
| | Configure VPP in all 'VNF' containers
| | Install VPP in all 'VNF' containers
+| Set up 3-node performance topology with wrk and DUT's NIC model
+| | [Documentation]
+| | ... | Suite preparation phase that setup default startup configuration of
+| | ... | VPP on all DUTs. Updates interfaces on all nodes and setup global
+| | ... | variables used in test cases based on interface model provided as an
+| | ... | argument. Installs the traffic generator.
+| | ...
+| | ... | *Arguments:*
+| | ... | - iface_model - Interface model. Type: string
+| | ...
+| | ... | *Example:*
+| | ...
+| | ... | \| Set up 3-node performance topology with wrk and DUT's NIC model\
+| | ... | \| Intel-X520-DA2 \|
+| | ...
+| | [Arguments] | ${iface_model}
+| | ...
+| | Set variables in 3-node circular topology with DUT interface model
+| | ... | ${iface_model}
+| | Iface update numa node | ${tg}
+# Make sure TRex is stopped
+| | ${running}= | Is TRex running | ${tg}
+| | Run keyword if | ${running}==${True} | Teardown traffic generator | ${tg}
+| | ${curr_driver}= | Get PCI dev driver | ${tg}
+| | ... | ${tg['interfaces']['${tg_if1}']['pci_address']}
+| | Run keyword if | '${curr_driver}'!='${None}'
+| | ... | PCI Driver Unbind | ${tg} |
+| | ... | ${tg['interfaces']['${tg_if1}']['pci_address']}
+# Bind tg_if1 to driver specified in the topology
+| | ${driver}= | Get Variable Value | ${tg['interfaces']['${tg_if1}']['driver']}
+| | PCI Driver Bind | ${tg}
+| | ... | ${tg['interfaces']['${tg_if1}']['pci_address']} | ${driver}
+# Set IP on tg_if1
+| | ${intf_name}= | Get Linux interface name | ${tg}
+| | ... | ${tg['interfaces']['${tg_if1}']['pci_address']}
+| | Set Linux interface IP | ${tg} | ${intf_name} | 192.168.10.1 | 24
+| | Set Linux interface up | ${tg} | ${intf_name}
+| | Install wrk | ${tg}
+
# Suite teardowns
| Tear down 3-node performance topology
@@ -480,6 +520,18 @@
| | Show VAT History On All DUTs | ${nodes}
| | Show statistics on all DUTs | ${nodes}
+| Tear down performance test with wrk
+| | [Documentation] | Common test teardown for ndrdisc and pdrdisc performance \
+| | ... | tests.
+| | ...
+| | ... | *Example:*
+| | ...
+| | ... | \| Tear down performance test with wrk \|
+| | ...
+| | Remove All Added Ports On All DUTs From Topology | ${nodes}
+| | Show VAT History On All DUTs | ${nodes}
+| | Show statistics on all DUTs | ${nodes}
+
| Tear down performance test with vhost and VM with dpdk-testpmd
| | [Documentation] | Common test teardown for performance tests which use
| | ... | vhost(s) and VM(s) with dpdk-testpmd.
diff --git a/resources/libraries/robot/tcp/tcp_setup.robot b/resources/libraries/robot/tcp/tcp_setup.robot
new file mode 100644
index 0000000000..09f6afd592
--- /dev/null
+++ b/resources/libraries/robot/tcp/tcp_setup.robot
@@ -0,0 +1,43 @@
+# Copyright (c) 2018 Cisco and/or its affiliates.
+# 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.
+
+*** Settings ***
+| Library | resources.libraries.python.IPv4Util.IPv4Util
+| Library | resources.libraries.python.InterfaceUtil
+| Library | resources.libraries.python.tcp.TCPUtils
+| Resource | resources/libraries/robot/ip/ip4.robot
+| ...
+| Documentation | L2 keywords to set up VPP to test tcp.
+
+*** Keywords ***
+| Set up HTTP server on the VPP node
+| | [Documentation]
+| | ... | Configure IP address on the port, set it up and start HTTP server on
+| | ... | the VPP.
+| | ...
+| | ... | *Arguments:*
+| | ... | - ${dut1_if1_ip4} - IP address to be set on the dut1_if1 interface.
+| | ... | Type: string
+| | ... | - ${ip4_len} - Length of the netmask. Type: integer
+| | ...
+| | ... | *Example:*
+| | ...
+| | ... | \| Set up HTTP server on the VPP node \| 192.168.10.2 \| 24 \|
+| | ...
+| | [Arguments] | ${dut1_if1_ip4} | ${ip4_len}
+| | ...
+| | Set Interface State | ${dut1} | ${dut1_if1} | up
+| | Set Interface Address | ${dut1} | ${dut1_if1} | ${dut1_if1_ip4} | ${ip4_len}
+| | Vpp Node Interfaces Ready Wait | ${dut1}
+| | Start HTTP server | ${dut1}
+| | Sleep | 30
diff --git a/resources/libraries/robot/wrk/wrk_utils.robot b/resources/libraries/robot/wrk/wrk_utils.robot
new file mode 100644
index 0000000000..fd18a5d686
--- /dev/null
+++ b/resources/libraries/robot/wrk/wrk_utils.robot
@@ -0,0 +1,78 @@
+# Copyright (c) 2018 Cisco and/or its affiliates.
+# 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.
+
+*** Settings ***
+| Library | resources.tools.wrk.wrk
+| Library | resources.libraries.python.IPUtil
+| Library | resources.libraries.python.DUTSetup
+| Library | resources.libraries.python.TrafficGenerator
+| Library | resources.libraries.python.topology.Topology
+| Resource | resources/libraries/robot/performance/performance_setup.robot
+| ...
+| Documentation | L2 keywords to set up wrk and to measure performance
+| ... | parameters using wrk.
+
+*** Keywords ***
+| Measure throughput
+| | [Documentation]
+| | ... | Measure throughput using wrk.
+| | ...
+| | ... | *Arguments:*
+| | ... | - ${profile} - The name of the wrk traffic profile defining the
+| | ... | traffic. Type: string
+| | ...
+| | ... | *Example:*
+| | ...
+| | ... | \| Measure throughput \| wrk-bw-1url-1core-50con \|
+| | ...
+| | [Arguments] | ${profile}
+| | ...
+| | ${tg_numa}= | Get interfaces numa node | ${tg} | ${tg_if1} | ${tg_if2}
+| | ${output}= | Run wrk | ${tg} | ${profile} | ${tg_numa} | bw
+| | Set test message | ${output}
+
+| Measure requests per second
+| | [Documentation]
+| | ... | Measure number of requests per second using wrk.
+| | ...
+| | ... | *Arguments:*
+| | ... | - ${profile} - The name of the wrk traffic profile defining the
+| | ... | traffic. Type: string
+| | ...
+| | ... | *Example:*
+| | ...
+| | ... | \| Measure requests per second \| wrk-bw-1url-1core-50con \|
+| | ...
+| | [Arguments] | ${profile}
+| | ...
+| | ${tg_numa}= | Get interfaces numa node | ${tg} | ${tg_if1} | ${tg_if2}
+| | ${output}= | Run wrk | ${tg} | ${profile} | ${tg_numa} | rps
+| | Set test message | ${output}
+
+| Measure connections per second
+| | [Documentation]
+| | ... | Measure number of connections per second using wrk.
+| | ...
+| | ... | *Arguments:*
+| | ... | - ${profile} - The name of the wrk traffic profile defining the
+| | ... | traffic. Type: string
+| | ...
+| | ... | *Example:*
+| | ...
+| | ... | \| Measure connections per second \| wrk-bw-1url-1core-50con \|
+| | ...
+| | [Arguments] | ${profile}
+| | ...
+| | ${tg_numa}= | Get interfaces numa node | ${tg} | ${tg_if1} | ${tg_if2}
+| | ${output}= | Run wrk | ${tg} | ${profile} | ${tg_numa} | cps
+| | Set test message | ${output}
diff --git a/resources/templates/vat/start_http_server.vat b/resources/templates/vat/start_http_server.vat
new file mode 100644
index 0000000000..1d00285a91
--- /dev/null
+++ b/resources/templates/vat/start_http_server.vat
@@ -0,0 +1 @@
+exec test http server static \ No newline at end of file
diff --git a/resources/tools/__init__.py b/resources/tools/__init__.py
new file mode 100644
index 0000000000..e9d6d476eb
--- /dev/null
+++ b/resources/tools/__init__.py
@@ -0,0 +1,16 @@
+# Copyright (c) 2018 Cisco and/or its affiliates.
+# 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.
+
+"""
+__init__ file for directory tools
+"""
diff --git a/resources/tools/wrk/__init__.py b/resources/tools/wrk/__init__.py
new file mode 100644
index 0000000000..977169c00f
--- /dev/null
+++ b/resources/tools/wrk/__init__.py
@@ -0,0 +1,16 @@
+# Copyright (c) 2018 Cisco and/or its affiliates.
+# 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.
+
+"""
+__init__ file for directory tools/wrk
+"""
diff --git a/resources/tools/wrk/doc/wrk_lld.rst b/resources/tools/wrk/doc/wrk_lld.rst
new file mode 100644
index 0000000000..1437fd8948
--- /dev/null
+++ b/resources/tools/wrk/doc/wrk_lld.rst
@@ -0,0 +1,293 @@
+Onboarding of wrk as a http traffic generator in CSIT
+-----------------------------------------------------
+
+wrk is a modern HTTP benchmarking tool capable of generating significant
+load when run on a single multi-core CPU.
+
+An optional LuaJIT script can perform HTTP request generation, response
+processing, and custom reporting.
+
+
+wrk installation on TG node
+'''''''''''''''''''''''''''
+
+**Procedure**
+
+ #. Check if wrk is installed on the TG node.
+ #. If not, install it.
+
+**wrk installation**
+
+::
+
+ # Install pre-requisites:
+ sudo apt-get install build-essential libssl-dev git -y
+
+ # Get the specified version:
+ wget ${WRK_DWNLD_PATH}/${WRK_TAR}
+ tar xzf ${WRK_TAR}
+ cd wrk-${WRK_VERSION}
+
+ # Build the wrk:
+ cd wrk
+ make
+
+ # Move the executable to somewhere in the PATH, e.q:
+ sudo cp wrk /usr/local/bin
+
+
+wrk traffic profile
+'''''''''''''''''''
+
+**The traffic profile can include these items:**
+
+ - List of URLs - mandatory,
+ - The first CPU used to run wrk - mandatory,
+ - Number of CPUs used for wrk - mandatory,
+ - Test duration - mandatory,
+ - Number of threads - mandatory,
+ - Number of connections - mandatory,
+ - LuaJIT script - optional, defaults to no script,
+ - HTTP header - optional, defaults to no header,
+ - Latency - optional, defaults to False,
+ - Timeout - optional, defaults to wrk default.
+
+**List of URLs**
+
+List of URLs for requests. Each URL is requested in a separate instance of wrk.
+Type: list
+
+*Example:*
+
+::
+
+ urls:
+ - "http://192.168.1.1/1kB.bin"
+ - "http://192.168.1.2/1kB.bin"
+ - "http://192.168.1.3/1kB.bin"
+
+**The first CPU used to run wrk**
+The first CPU used to run wrk. The other CPUs follow this one.
+Type: integer
+
+*Example:*
+
+::
+
+ first-cpu: 1
+
+**Number of CPUs used for wrk**
+
+The number of CPUs used for wrk. The number of CPUs must be a multiplication
+of the number of URLs.
+Type: integer
+
+*Example:*
+
+::
+
+ cpus: 6
+
+.. note::
+
+ The combinations of URLs and a number of CPUs create following use cases:
+
+ - One URL and one CPU - One instance of wrk sends one request (URL) via
+ one NIC
+ - One URL and n CPUs - n instances of wrk send the same request (URL)
+ via one or more NICs
+ - n URLs and n CPUs - n instances of wrk send n requests (URL) via one
+ or more NICs
+ - n URLs and m CPUs, m = a * n - m instances of wrk send n requests
+ (URL) via one or more NICs
+
+**Test duration**
+
+Duration of the test in seconds.
+Type: integer
+
+*Example:*
+
+::
+
+ duration: 30
+
+**Number of threads**
+
+Total number of threads to use by wrk to send traffic.
+Type: integer
+
+*Example:*
+
+::
+
+ nr-of-threads: 1
+
+**Number of connections**
+
+Total number of HTTP connections to keep open with each thread handling
+N = connections / threads.
+Type: integer
+
+*Example:*
+
+::
+
+ nr-of-connections: 50
+
+**LuaJIT script**
+
+Path to LuaJIT script.
+Type: string
+
+For more information see: https://github.com/wg/wrk/blob/master/SCRIPTING
+
+*Example:*
+
+::
+
+ script: "scripts/report.lua"
+
+**HTTP header**
+
+HTTP header to add to request.
+Type: string (taken as it is) or dictionary
+
+*Example:*
+
+::
+
+ # Dictionary:
+ header:
+ Connection: "close"
+
+or
+
+::
+
+ # String:
+ header: "Connection: close"
+
+**Latency**
+
+Print detailed latency statistics.
+Type: boolean
+
+*Example:*
+
+::
+
+ latency: False
+
+**Timeout**
+
+Record a timeout if a response is not received within this amount of time.
+Type: integer
+
+::
+
+ timeout: 5
+
+**Examples of a wrk traffic profile**
+
+*Get the number of connections per second:*
+
+- Use 3 CPUs to send 3 different requests via 3 NICs.
+- The test takes 30 seconds.
+- wrk sends traffic in one thread per CPU.
+- There will be open max 50 connection at the same time.
+- The header is set to 'Connection: "close"' so wrk opens separate connection
+ for each request. Then the number of requests equals to the number of
+ connections.
+- Timeout for responses from the server is set to 5 seconds.
+
+::
+
+ urls:
+ - "http://192.168.1.1/0B.bin"
+ - "http://192.168.1.2/0B.bin"
+ - "http://192.168.1.3/0B.bin"
+ cpus: 3
+ duration: 30
+ nr-of-threads: 1
+ nr-of-connections: 50
+ header:
+ Connection: "close"
+ timeout: 5
+
+*Get the number of requests per second:*
+
+- Use 3 CPUs to send 3 different requests via 3 NICs.
+- The test takes 30 seconds.
+- wrk sends traffic in one thread per CPU.
+- There will be max 50 concurrent open connections.
+
+::
+
+ urls:
+ - "http://192.168.1.1/1kB.bin"
+ - "http://192.168.1.2/1kB.bin"
+ - "http://192.168.1.3/1kB.bin"
+ cpus: 3
+ duration: 30
+ nr-of-threads: 1
+ nr-of-connections: 50
+
+*Get the bandwidth:*
+
+- Use 3 CPUs to send 3 different requests via 3 NICs.
+- The test takes 30 seconds.
+- wrk sends traffic in one thread per CPU.
+- There will be open max 50 connection at the same time.
+- Timeout for responses from the server is set to 5 seconds.
+
+::
+
+ urls:
+ - "http://192.168.1.1/1MB.bin"
+ - "http://192.168.1.2/1MB.bin"
+ - "http://192.168.1.3/1MB.bin"
+ cpus: 3
+ duration: 30
+ nr-of-threads: 1
+ nr-of-connections: 50
+ timeout: 5
+
+
+Running wrk
+'''''''''''
+
+**Suite setup phase**
+
+CSIT framework checks if wrk is installed on the TG node. If not, or if the
+installation is forced, it installs it on the TG node.
+
+*Procedure:*
+
+ #. Make sure TRex is stopped.
+ #. Bind used TG interfaces to corresponding drivers (defined in the topology
+ file).
+ #. If the wrk installation is forced:
+
+ - Destroy existing wrk
+
+ #. If the wrk installation is not forced:
+
+ - Check if wrk is installed.
+ - If installed, exit.
+
+ #. Clone wrk from git (https://github.com/wg/wrk.git)
+ #. Build wrk.
+ #. Copy the executable to /usr/local/bin so it is in the PATH.
+
+**Test phase**
+
+*Procedure:*
+
+#. Read the wrk traffic profile.
+#. Verify the profile.
+#. Use the information from the profile to set the wrk parameters.
+#. Run wrk.
+#. Read the output.
+#. Evaluate and log the output.
+
diff --git a/resources/tools/wrk/wrk.py b/resources/tools/wrk/wrk.py
new file mode 100644
index 0000000000..33cfd08174
--- /dev/null
+++ b/resources/tools/wrk/wrk.py
@@ -0,0 +1,291 @@
+# Copyright (c) 2018 Cisco and/or its affiliates.
+# 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.
+
+"""wrk implementation into CSIT framework.
+"""
+
+import re
+
+from robot.api import logger
+
+from resources.libraries.python.ssh import SSH
+from resources.libraries.python.topology import NodeType
+from resources.libraries.python.CpuUtils import CpuUtils
+from resources.libraries.python.constants import Constants
+
+from resources.tools.wrk.wrk_traffic_profile_parser import WrkTrafficProfile
+from resources.tools.wrk.wrk_errors import WrkError
+
+
+REGEX_LATENCY_STATS = \
+ r"Latency\s*" \
+ r"(\d*\.*\d*\S*)\s*" \
+ r"(\d*\.*\d*\S*)\s*" \
+ r"(\d*\.*\d*\S*)\s*" \
+ r"(\d*\.*\d*\%)"
+REGEX_RPS_STATS = \
+ r"Req/Sec\s*" \
+ r"(\d*\.*\d*\S*)\s*" \
+ r"(\d*\.*\d*\S*)\s*" \
+ r"(\d*\.*\d*\S*)\s*" \
+ r"(\d*\.*\d*\%)"
+REGEX_RPS = r"Requests/sec:\s*" \
+ r"(\d*\.*\S*)"
+REGEX_BW = r"Transfer/sec:\s*" \
+ r"(\d*\.*\S*)"
+REGEX_LATENCY_DIST = \
+ r"Latency Distribution\n" \
+ r"\s*50\%\s*(\d*\.*\d*\D*)\n" \
+ r"\s*75\%\s*(\d*\.*\d*\D*)\n" \
+ r"\s*90\%\s*(\d*\.*\d*\D*)\n" \
+ r"\s*99\%\s*(\d*\.*\d*\D*)\n"
+
+# Split number and multiplicand, e.g. 14.25k --> 14.25 and k
+REGEX_NUM = r"(\d*\.*\d*)(\D*)"
+
+
+def install_wrk(tg_node):
+ """Install wrk on the TG node.
+
+ :param tg_node: Traffic generator node.
+ :type tg_node: dict
+ :raises: RuntimeError if the given node is not a TG node or if the
+ installation fails.
+ """
+
+ if tg_node['type'] != NodeType.TG:
+ raise RuntimeError('Node type is not a TG.')
+
+ ssh = SSH()
+ ssh.connect(tg_node)
+
+ ret, _, _ = ssh.exec_command(
+ "sudo -E "
+ "sh -c '{0}/resources/tools/wrk/wrk_utils.sh install false'".
+ format(Constants.REMOTE_FW_DIR), timeout=1800)
+ if int(ret) != 0:
+ raise RuntimeError('Installation of wrk on TG node failed.')
+
+
+def destroy_wrk(tg_node):
+ """Destroy wrk on the TG node.
+
+ :param tg_node: Traffic generator node.
+ :type tg_node: dict
+ :raises: RuntimeError if the given node is not a TG node or the removal of
+ wrk failed.
+ """
+
+ if tg_node['type'] != NodeType.TG:
+ raise RuntimeError('Node type is not a TG.')
+
+ ssh = SSH()
+ ssh.connect(tg_node)
+
+ ret, _, _ = ssh.exec_command(
+ "sudo -E "
+ "sh -c '{0}/resources/tools/wrk/wrk_utils.sh destroy'".
+ format(Constants.REMOTE_FW_DIR), timeout=1800)
+ if int(ret) != 0:
+ raise RuntimeError('Removal of wrk from the TG node failed.')
+
+
+def run_wrk(tg_node, profile_name, tg_numa, test_type):
+ """Send the traffic as defined in the profile.
+
+ :param tg_node: Traffic generator node.
+ :param profile_name: The name of wrk traffic profile.
+ :param tg_numa: Numa node on which wrk will run.
+ :param test_type: The type of the tests: cps, rps, bw
+ :type profile_name: str
+ :type tg_node: dict
+ :type tg_numa: int
+ :type test_type: str
+ :returns: Message with measured data.
+ :rtype: str
+ :raises: RuntimeError if node type is not a TG.
+ """
+
+ if tg_node['type'] != NodeType.TG:
+ raise RuntimeError('Node type is not a TG.')
+
+ # Parse and validate the profile
+ profile_path = ("resources/traffic_profiles/wrk/{0}.yaml".
+ format(profile_name))
+ profile = WrkTrafficProfile(profile_path).traffic_profile
+
+ cores = CpuUtils.cpu_list_per_node(tg_node, tg_numa)
+ first_cpu = cores[profile["first-cpu"]]
+
+ if len(profile["urls"]) == 1 and profile["cpus"] == 1:
+ params = [
+ "traffic_1_url_1_core",
+ str(first_cpu),
+ str(profile["nr-of-threads"]),
+ str(profile["nr-of-connections"]),
+ "{0}s".format(profile["duration"]),
+ "'{0}'".format(profile["header"]),
+ str(profile["timeout"]),
+ str(profile["script"]),
+ str(profile["latency"]),
+ "'{0}'".format(" ".join(profile["urls"]))
+ ]
+ elif len(profile["urls"]) == profile["cpus"]:
+ params = [
+ "traffic_n_urls_n_cores",
+ str(first_cpu),
+ str(profile["nr-of-threads"]),
+ str(profile["nr-of-connections"]),
+ "{0}s".format(profile["duration"]),
+ "'{0}'".format(profile["header"]),
+ str(profile["timeout"]),
+ str(profile["script"]),
+ str(profile["latency"]),
+ "'{0}'".format(" ".join(profile["urls"]))
+ ]
+ else:
+ params = [
+ "traffic_n_urls_m_cores",
+ str(first_cpu),
+ str(profile["cpus"] / len(profile["urls"])),
+ str(profile["nr-of-threads"]),
+ str(profile["nr-of-connections"]),
+ "{0}s".format(profile["duration"]),
+ "'{0}'".format(profile["header"]),
+ str(profile["timeout"]),
+ str(profile["script"]),
+ str(profile["latency"]),
+ "'{0}'".format(" ".join(profile["urls"]))
+ ]
+ args = " ".join(params)
+
+ ssh = SSH()
+ ssh.connect(tg_node)
+
+ ret, stdout, _ = ssh.exec_command(
+ "{0}/resources/tools/wrk/wrk_utils.sh {1}".
+ format(Constants.REMOTE_FW_DIR, args), timeout=1800)
+ if int(ret) != 0:
+ raise RuntimeError('wrk runtime error.')
+
+ stats = _parse_wrk_output(stdout)
+
+ log_msg = "\nMeasured values:\n"
+ if test_type == "cps":
+ log_msg += "Connections/sec: Avg / Stdev / Max / +/- Stdev\n"
+ for item in stats["rps-stats-lst"]:
+ log_msg += "{0} / {1} / {2} / {3}\n".format(*item)
+ log_msg += "Total cps: {0}cps\n".format(stats["rps-sum"])
+ elif test_type == "rps":
+ log_msg += "Requests/sec: Avg / Stdev / Max / +/- Stdev\n"
+ for item in stats["rps-stats-lst"]:
+ log_msg += "{0} / {1} / {2} / {3}\n".format(*item)
+ log_msg += "Total rps: {0}cps\n".format(stats["rps-sum"])
+ elif test_type == "bw":
+ log_msg += "Transfer/sec: {0}Bps".format(stats["bw-sum"])
+
+ logger.info(log_msg)
+
+ return log_msg
+
+
+def _parse_wrk_output(msg):
+ """Parse the wrk stdout with the results.
+
+ :param msg: stdout of wrk.
+ :type msg: str
+ :returns: Parsed results.
+ :rtype: dict
+ :raises: WrkError if the message does not include the results.
+ """
+
+ if "Thread Stats" not in msg:
+ raise WrkError("The output of wrk does not include the results.")
+
+ msg_lst = msg.splitlines(False)
+
+ stats = {
+ "latency-dist-lst": list(),
+ "latency-stats-lst": list(),
+ "rps-stats-lst": list(),
+ "rps-lst": list(),
+ "bw-lst": list(),
+ "rps-sum": 0,
+ "bw-sum": None
+ }
+
+ for line in msg_lst:
+ if "Latency Distribution" in line:
+ # Latency distribution - 50%, 75%, 90%, 99%
+ pass
+ elif "Latency" in line:
+ # Latency statistics - Avg, Stdev, Max, +/- Stdev
+ pass
+ elif "Req/Sec" in line:
+ # rps statistics - Avg, Stdev, Max, +/- Stdev
+ stats["rps-stats-lst"].append((
+ _evaluate_number(re.search(REGEX_RPS_STATS, line).group(1)),
+ _evaluate_number(re.search(REGEX_RPS_STATS, line).group(2)),
+ _evaluate_number(re.search(REGEX_RPS_STATS, line).group(3)),
+ _evaluate_number(re.search(REGEX_RPS_STATS, line).group(4))))
+ elif "Requests/sec:" in line:
+ # rps (cps)
+ stats["rps-lst"].append(
+ _evaluate_number(re.search(REGEX_RPS, line).group(1)))
+ elif "Transfer/sec:" in line:
+ # BW
+ stats["bw-lst"].append(
+ _evaluate_number(re.search(REGEX_BW, line).group(1)))
+
+ for item in stats["rps-stats-lst"]:
+ stats["rps-sum"] += item[0]
+ stats["bw-sum"] = sum(stats["bw-lst"])
+
+ return stats
+
+
+def _evaluate_number(num):
+ """Evaluate the numeric value of the number with multiplicands, e.g.:
+ 12.25k --> 12250
+
+ :param num: Number to evaluate.
+ :type num: str
+ :returns: Evaluated number.
+ :rtype: float
+ :raises: WrkError if it is not possible to evaluate the given number.
+ """
+
+ val = re.search(REGEX_NUM, num)
+ try:
+ val_num = float(val.group(1))
+ except ValueError:
+ raise WrkError("The output of wrk does not include the results "
+ "or the format of results has changed.")
+ val_mul = val.group(2).lower()
+ if val_mul:
+ if "k" in val_mul:
+ val_num *= 1000
+ elif "m" in val_mul:
+ val_num *= 1000000
+ elif "g" in val_mul:
+ val_num *= 1000000000
+ elif "b" in val_mul:
+ pass
+ elif "%" in val_mul:
+ pass
+ elif "" in val_mul:
+ pass
+ else:
+ raise WrkError("The multiplicand {0} is not defined.".
+ format(val_mul))
+ return val_num
diff --git a/resources/tools/wrk/wrk_errors.py b/resources/tools/wrk/wrk_errors.py
new file mode 100644
index 0000000000..3173dd4223
--- /dev/null
+++ b/resources/tools/wrk/wrk_errors.py
@@ -0,0 +1,55 @@
+# Copyright (c) 2018 Cisco and/or its affiliates.
+# 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.
+
+"""Implementation of exceptions used in the wrk traffic generator.
+"""
+
+
+from robot.api import logger
+
+
+class WrkError(Exception):
+ """Exception(s) raised by the wrk traffic generator.
+
+ When raising this exception, put this information to the message in this
+ order:
+ - short description of the encountered problem (parameter msg),
+ - relevant messages if there are any collected, e.g., from caught
+ exception (optional parameter details),
+ - relevant data if there are any collected (optional parameter details).
+ """
+
+ def __init__(self, msg, details=''):
+ """Sets the exception message and the level.
+
+ :param msg: Short description of the encountered problem.
+ :param details: Relevant messages if there are any collected, e.g.:
+ from caught exception (optional parameter details), or relevant data if
+ there are any collected (optional parameter details).
+ :type msg: str
+ :type details: str
+ """
+
+ super(WrkError, self).__init__()
+ self._msg = msg
+ self._details = details
+
+ logger.error(self._msg)
+ if self._details:
+ logger.error(self._details)
+
+ def __repr__(self):
+ return repr(self._msg)
+
+ def __str__(self):
+ return str(self._msg)
diff --git a/resources/tools/wrk/wrk_traffic_profile_parser.py b/resources/tools/wrk/wrk_traffic_profile_parser.py
new file mode 100644
index 0000000000..e1f8365345
--- /dev/null
+++ b/resources/tools/wrk/wrk_traffic_profile_parser.py
@@ -0,0 +1,286 @@
+# Copyright (c) 2018 Cisco and / or its affiliates.
+# 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.
+
+"""wrk traffic profile parser.
+
+See LLD for the structure of a wrk traffic profile.
+"""
+
+
+from os.path import isfile
+from pprint import pformat
+
+from yaml import load, YAMLError
+from robot.api import logger
+
+from resources.tools.wrk.wrk_errors import WrkError
+
+
+class WrkTrafficProfile(object):
+ """The wrk traffic profile.
+ """
+
+ MANDATORY_PARAMS = ("urls",
+ "first-cpu",
+ "cpus",
+ "duration",
+ "nr-of-threads",
+ "nr-of-connections")
+
+ def __init__(self, profile_name):
+ """Read the traffic profile from the yaml file.
+
+ :param profile_name: Path to the yaml file with the profile.
+ :type profile_name: str
+ :raises: WrkError if it is not possible to parse the profile.
+ """
+
+ self._profile_name = None
+ self._traffic_profile = None
+
+ self.profile_name = profile_name
+
+ try:
+ with open(self.profile_name, 'r') as profile_file:
+ self.traffic_profile = load(profile_file)
+ except IOError as err:
+ raise WrkError(msg="An error occurred while opening the file '{0}'."
+ .format(self.profile_name),
+ details=str(err))
+ except YAMLError as err:
+ raise WrkError(msg="An error occurred while parsing the traffic "
+ "profile '{0}'.".format(self.profile_name),
+ details=str(err))
+
+ self._validate_traffic_profile()
+
+ if self.traffic_profile:
+ logger.debug("\nThe wrk traffic profile '{0}' is valid.\n".
+ format(self.profile_name))
+ logger.debug("wrk traffic profile '{0}':".format(self.profile_name))
+ logger.debug(pformat(self.traffic_profile))
+ else:
+ logger.debug("\nThe wrk traffic profile '{0}' is invalid.\n".
+ format(self.profile_name))
+ raise WrkError("\nThe wrk traffic profile '{0}' is invalid.\n".
+ format(self.profile_name))
+
+ def __repr__(self):
+ return pformat(self.traffic_profile)
+
+ def __str__(self):
+ return pformat(self.traffic_profile)
+
+ def _validate_traffic_profile(self):
+ """Validate the traffic profile.
+
+ The specification, the structure and the rules are described in
+ doc/wrk_lld.rst
+ """
+
+ logger.debug("\nValidating the wrk traffic profile '{0}'...\n".
+ format(self.profile_name))
+
+ # Level 1: Check if the profile is a dictionary:
+ if not isinstance(self.traffic_profile, dict):
+ logger.error("The wrk traffic profile must be a dictionary.")
+ self.traffic_profile = None
+ return
+
+ # Level 2: Check if all mandatory parameters are present:
+ is_valid = True
+ for param in self.MANDATORY_PARAMS:
+ if self.traffic_profile.get(param, None) is None:
+ logger.error("The parameter '{0}' in mandatory.".format(param))
+ is_valid = False
+ if not is_valid:
+ self.traffic_profile = None
+ return
+
+ # Level 3: Mandatory params: Check if urls is a list:
+ is_valid = True
+ if not isinstance(self.traffic_profile["urls"], list):
+ logger.error("The parameter 'urls' must be a list.")
+ is_valid = False
+
+ # Level 3: Mandatory params: Check if cpus is a valid integer:
+ try:
+ cpus = int(self.traffic_profile["cpus"])
+ if cpus < 1:
+ raise ValueError
+ self.traffic_profile["cpus"] = cpus
+ except ValueError:
+ logger.error("The parameter 'cpus' must be an integer greater than "
+ "1.")
+ is_valid = False
+
+ # Level 3: Mandatory params: Check if first-cpu is a valid integer:
+ try:
+ first_cpu = int(self.traffic_profile["first-cpu"])
+ if first_cpu < 0:
+ raise ValueError
+ self.traffic_profile["first-cpu"] = first_cpu
+ except ValueError:
+ logger.error("The parameter 'first-cpu' must be an integer greater "
+ "than 1.")
+ is_valid = False
+
+ # Level 3: Mandatory params: Check if duration is a valid integer:
+ try:
+ duration = int(self.traffic_profile["duration"])
+ if duration < 1:
+ raise ValueError
+ self.traffic_profile["duration"] = duration
+ except ValueError:
+ logger.error("The parameter 'duration' must be an integer "
+ "greater than 1.")
+ is_valid = False
+
+ # Level 3: Mandatory params: Check if nr-of-threads is a valid integer:
+ try:
+ nr_of_threads = int(self.traffic_profile["nr-of-threads"])
+ if nr_of_threads < 1:
+ raise ValueError
+ self.traffic_profile["nr-of-threads"] = nr_of_threads
+ except ValueError:
+ logger.error("The parameter 'nr-of-threads' must be an integer "
+ "greater than 1.")
+ is_valid = False
+
+ # Level 3: Mandatory params: Check if nr-of-connections is a valid
+ # integer:
+ try:
+ nr_of_connections = int(self.traffic_profile["nr-of-connections"])
+ if nr_of_connections < 1:
+ raise ValueError
+ self.traffic_profile["nr-of-connections"] = nr_of_connections
+ except ValueError:
+ logger.error("The parameter 'nr-of-connections' must be an integer "
+ "greater than 1.")
+ is_valid = False
+
+ # Level 4: Optional params: Check if script is present:
+ script = self.traffic_profile.get("script", None)
+ if script is not None:
+ if not isinstance(script, str):
+ logger.error("The path to LuaJIT script in invalid")
+ is_valid = False
+ else:
+ if not isfile(script):
+ logger.error("The file '{0}' in not present.".
+ format(script))
+ is_valid = False
+ else:
+ self.traffic_profile["script"] = None
+ logger.debug("The optional parameter 'LuaJIT script' is not "
+ "defined. No problem.")
+
+ # Level 4: Optional params: Check if header is present:
+ header = self.traffic_profile.get("header", None)
+ if header:
+ if not (isinstance(header, dict) or isinstance(header, str)):
+ logger.error("The parameter 'header' is not valid.")
+ is_valid = False
+ else:
+ if isinstance(header, dict):
+ header_lst = list()
+ for key, val in header.items():
+ header_lst.append("{0}: {1}".format(key, val))
+ if header_lst:
+ self.traffic_profile["header"] = ", ".join(header_lst)
+ else:
+ logger.error("The parameter 'header' is defined but "
+ "empty.")
+ is_valid = False
+ else:
+ self.traffic_profile["header"] = None
+ logger.debug("The optional parameter 'header' is not defined. "
+ "No problem.")
+
+ # Level 4: Optional params: Check if latency is present:
+ latency = self.traffic_profile.get("latency", None)
+ if latency is not None:
+ try:
+ latency = bool(latency)
+ self.traffic_profile["latency"] = latency
+ except ValueError:
+ logger.error("The parameter 'latency' must be boolean.")
+ is_valid = False
+ else:
+ self.traffic_profile["latency"] = False
+ logger.debug("The optional parameter 'latency' is not defined. "
+ "No problem.")
+
+ # Level 4: Optional params: Check if timeout is present:
+ timeout = self.traffic_profile.get("timeout", None)
+ if timeout:
+ try:
+ timeout = int(timeout)
+ if timeout < 1:
+ raise ValueError
+ self.traffic_profile["timeout"] = timeout
+ except ValueError:
+ logger.error("The parameter 'timeout' must be integer greater "
+ "than 1.")
+ is_valid = False
+ else:
+ self.traffic_profile["timeout"] = None
+ logger.debug("The optional parameter 'timeout' is not defined. "
+ "No problem.")
+
+ if not is_valid:
+ self.traffic_profile = None
+ return
+
+ # Level 5: Check dependencies between parameters:
+ # Level 5: Check urls and cpus:
+ if self.traffic_profile["cpus"] % len(self.traffic_profile["urls"]):
+ logger.error("The number of CPUs must be a multiplication of the "
+ "number of URLs.")
+ self.traffic_profile = None
+
+ @property
+ def profile_name(self):
+ """Getter - Profile name.
+
+ :returns: The traffic profile file path
+ :rtype: str
+ """
+ return self._profile_name
+
+ @profile_name.setter
+ def profile_name(self, profile_name):
+ """
+
+ :param profile_name:
+ :type profile_name: str
+ """
+ self._profile_name = profile_name
+
+ @property
+ def traffic_profile(self):
+ """Getter: Traffic profile.
+
+ :returns: The traffic profile.
+ :rtype: dict
+ """
+ return self._traffic_profile
+
+ @traffic_profile.setter
+ def traffic_profile(self, profile):
+ """Setter - Traffic profile.
+
+ :param profile: The new traffic profile.
+ :type profile: dict
+ """
+ self._traffic_profile = profile
diff --git a/resources/tools/wrk/wrk_utils.sh b/resources/tools/wrk/wrk_utils.sh
new file mode 100755
index 0000000000..2b9f6cf296
--- /dev/null
+++ b/resources/tools/wrk/wrk_utils.sh
@@ -0,0 +1,290 @@
+#!/bin/bash
+# Copyright (c) 2018 Cisco and/or its affiliates.
+# 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.
+
+set -x
+
+WRK_VERSION="4.0.2"
+WRK_TAR=${WRK_VERSION}".tar.gz"
+WRK_DWNLD_PATH="https://github.com/wg/wrk/archive"
+WRK_TARGET="/opt"
+WRK_INSTALL_DIR=${WRK_TARGET}/wrk-${WRK_VERSION}
+
+function wrk_utils.install {
+ # Install wrk
+
+ # Directory for wrk:
+ dir=${1}
+ # Force the installation:
+ force=${2:-false}
+
+ # Check if wrk is installed:
+ if [ "${force}" = true ]; then
+ wrk_utils.destroy
+ else
+ which wrk
+ if [ $? -eq 0 ]; then
+ test -d ${dir}/${WRK_INSTALL_DIR} && echo "WRK already installed: ${dir}/${WRK_INSTALL_DIR}" && exit 0
+ fi
+ fi
+
+ # Install pre-requisites:
+ apt-get update
+ apt-get install build-essential libssl-dev -y
+
+ # Remove previous installation:
+ wrk_utils.destroy
+
+ # Change the directory:
+ cd ${WRK_TARGET}
+
+ # Get the specified version:
+ wget ${WRK_DWNLD_PATH}/${WRK_TAR}
+ tar xzf ${WRK_TAR}
+ rm ${WRK_TAR}
+ cd ${WRK_INSTALL_DIR}
+
+ # Build the wrk:
+ make
+
+ # Move the executable to somewhere in the PATH:
+ cp wrk /usr/local/bin
+}
+
+function wrk_utils.destroy {
+ # Remove wrk
+
+ sudo rm /usr/local/bin/wrk || true
+ sudo rm -rf ${WRK_INSTALL_DIR} || true
+}
+
+function wrk_utils.traffic_1_url_1_core {
+ # Send traffic
+ # - to n URL (NIC)
+ # - using n instances of wrk, each on separate core.
+
+ # The CPU used for wrk
+ cpu=${1}
+ # Total number of threads to use by one instance of wrk to send traffic.
+ threads=${2}
+ # Total number of HTTP connections to keep open with each thread handling
+ # N = connections / threads.
+ connections=${3}
+ # Duration of the test.
+ duration=${4}
+ # HTTP header to add to request.
+ header=${5}
+ # Record a timeout if a response is not received within this amount of time.
+ timeout=${6}
+ # Path to LuaJIT script.
+ script=${7}
+ # Print detailed latency statistics.
+ latency=${8}
+ # URL to send the traffic to.
+ url=${9}
+
+ if [ "${timeout}" != "None" ]; then
+ timeout="--timeout ${timeout}"
+ else
+ timeout=""
+ fi
+
+ if [ "${latency}" = "True" ]; then
+ latency="--latency"
+ else
+ latency=""
+ fi
+
+ if [ "${script}" != "None" ]; then
+ script="--script '${script}'"
+ else
+ script=""
+ fi
+
+ if [ "${header}" != "None" ]; then
+ header="${header}"
+ else
+ header=""
+ fi
+
+ taskset --cpu-list ${cpu} \
+ wrk --threads ${threads} \
+ --connections ${connections} \
+ --duration ${duration} \
+ --header "${header}" \
+ ${timeout} \
+ ${script} \
+ ${latency} \
+ ${url}
+}
+
+function wrk_utils.traffic_n_urls_n_cores {
+ # Send traffic
+ # - to n URL (NIC)
+ # - using n instances of wrk, each on separate core.
+
+ # The first CPU used for wrk
+ first_cpu=${1}
+ # Total number of threads to use by one instance of wrk to send traffic.
+ threads=${2}
+ # Total number of HTTP connections to keep open with each thread handling
+ # N = connections / threads.
+ connections=${3}
+ # Duration of the test.
+ duration=${4}
+ # HTTP header to add to request.
+ header=${5}
+ # Record a timeout if a response is not received within this amount of time.
+ timeout=${6}
+ # Path to LuaJIT script.
+ script=${7}
+ # Print detailed latency statistics.
+ latency=${8}
+ # URL to send the traffic to.
+ urls=${9}
+
+ if [ "${timeout}" != "None" ]; then
+ timeout="--timeout ${timeout}"
+ else
+ timeout=""
+ fi
+
+ if [ "${latency}" = "True" ]; then
+ latency="--latency"
+ else
+ latency=""
+ fi
+
+ if [ "${script}" != "None" ]; then
+ script="--script '${script}'"
+ else
+ script=""
+ fi
+
+ if [ "${header}" != "None" ]; then
+ header="${header}"
+ else
+ header=""
+ fi
+
+ urls=$(echo ${urls} | tr ";" "\n")
+ cpu=${first_cpu}
+ for url in ${urls}; do
+ taskset --cpu-list ${cpu} \
+ wrk --threads ${threads} \
+ --connections ${connections} \
+ --duration ${duration} \
+ --header "${header}" \
+ ${timeout} \
+ ${script} \
+ ${latency} \
+ ${url} &
+ cpu=$((cpu+1))
+ done
+
+ sleep ${duration}
+ sleep 2
+}
+
+function wrk_utils.traffic_n_urls_m_cores {
+ # Send traffic
+ # - to n URL (NIC)
+ # - using m instances of wrk, each on separate core.
+
+ # The first CPU used for wrk
+ first_cpu=${1}
+ # The last CPU used for wrk
+ cpus_per_url=${2}
+ # Total number of threads to use by one instance of wrk to send traffic.
+ threads=${3}
+ # Total number of HTTP connections to keep open with each thread handling
+ # N = connections / threads.
+ connections=${4}
+ # Duration of the test.
+ duration=${5}
+ # HTTP header to add to request.
+ header=${6}
+ # Record a timeout if a response is not received within this amount of time.
+ timeout=${7}
+ # Path to LuaJIT script.
+ script=${8}
+ # Print detailed latency statistics.
+ latency=${9}
+ # URL to send the traffic to.
+ urls=${10}
+
+ if [ "${timeout}" != "None" ]; then
+ timeout="--timeout ${timeout}"
+ else
+ timeout=""
+ fi
+
+ if [ "${latency}" = "True" ]; then
+ latency="--latency"
+ else
+ latency=""
+ fi
+
+ if [ "${script}" != "None" ]; then
+ script="--script '${script}'"
+ else
+ script=""
+ fi
+
+ if [ "${header}" != "None" ]; then
+ header="${header}"
+ else
+ header=""
+ fi
+
+ urls=$(echo ${urls} | tr ";" "\n")
+
+ cpu=${first_cpu}
+ for i in `seq 1 ${cpus_per_url}`; do
+ for url in ${urls}; do
+ taskset --cpu-list ${cpu} \
+ wrk --threads ${threads} \
+ --connections ${connections} \
+ --duration ${duration} \
+ --header "${header}" \
+ ${timeout} \
+ ${script} \
+ ${latency} \
+ ${url} &
+ cpu=$((cpu+1))
+ done
+ done
+
+ sleep ${duration}
+ sleep 2
+}
+
+args=("$@")
+case ${1} in
+ install)
+ force=${2}
+ wrk_utils.install ${force}
+ ;;
+ destroy)
+ wrk_utils.destroy
+ ;;
+ traffic_1_url_1_core)
+ wrk_utils.traffic_1_url_1_core "${args[@]:1}"
+ ;;
+ traffic_n_urls_n_cores)
+ wrk_utils.traffic_n_urls_n_cores "${args[@]:1}"
+ ;;
+ traffic_n_urls_m_cores)
+ wrk_utils.traffic_n_urls_m_cores "${args[@]:1}"
+ ;;
+esac
diff --git a/resources/traffic_profiles/wrk/wrk-sf-2n-ethip4tcphttp-1u1c1con-cps.yaml b/resources/traffic_profiles/wrk/wrk-sf-2n-ethip4tcphttp-1u1c1con-cps.yaml
new file mode 100644
index 0000000000..cf120e7e1d
--- /dev/null
+++ b/resources/traffic_profiles/wrk/wrk-sf-2n-ethip4tcphttp-1u1c1con-cps.yaml
@@ -0,0 +1,47 @@
+# This is an example wrk traffic profile.
+
+# List of urls for requests. Each url is requested in a separate instance of
+# wrk.
+# Type: list
+urls:
+ # There must be an empty file (0B) requested but vpp does not support it.
+ - "http://192.168.10.2"
+
+# Index of the first CPU on the numa used to run wrk.
+# Type: integer
+first-cpu: 1
+
+# The number of cpus used for wrk. The number of cpus must be a
+# multiplication of the number of urls.
+# Type: integer
+cpus: 1
+
+# Duration of the test in seconds.
+# Type: integer
+duration: 30
+
+# Total number of threads to use.
+# Type: integer
+nr-of-threads: 1
+
+# Total number of HTTP connections to keep open with each thread handling
+# N = connections/threads.
+# Type: integer
+nr-of-connections: 1
+
+# Path to LuaJIT script.
+# Type: string
+# script: ""
+
+# HTTP header to add to request, e.g. "Connection: close".
+# Type: string (taken as it is) or dictionary
+header:
+ Connection: "close"
+
+# Print detailed latency statistics.
+# Type: boolean
+latency: False
+
+# Record a timeout if a response is not received within this amount of time.
+# Type: integer
+timeout: 5
diff --git a/resources/traffic_profiles/wrk/wrk-sf-2n-ethip4tcphttp-1u1c50con-bw.yaml b/resources/traffic_profiles/wrk/wrk-sf-2n-ethip4tcphttp-1u1c50con-bw.yaml
new file mode 100644
index 0000000000..93ce51d6d0
--- /dev/null
+++ b/resources/traffic_profiles/wrk/wrk-sf-2n-ethip4tcphttp-1u1c50con-bw.yaml
@@ -0,0 +1,47 @@
+# This is an example wrk traffic profile.
+
+# List of urls for requests. Each url is requested in a separate instance of
+# wrk.
+# Type: list
+urls:
+ # There must be a big file (10MB) requested but vpp does not support it.
+ - "http://192.168.10.2"
+
+# Index of the first CPU on the numa used to run wrk.
+# Type: integer
+first-cpu: 1
+
+# The number of cpus used for wrk. The number of cpus must be a
+# multiplication of the number of urls.
+# Type: integer
+cpus: 1
+
+# Duration of the test in seconds.
+# Type: integer
+duration: 30
+
+# Total number of threads to use.
+# Type: integer
+nr-of-threads: 1
+
+# Total number of HTTP connections to keep open with each thread handling
+# N = connections/threads.
+# Type: integer
+nr-of-connections: 50
+
+# Path to LuaJIT script.
+# Type: string
+# script: ""
+
+# HTTP header to add to request, e.g. "Connection: close".
+# Type: string (taken as it is) or dictionary
+# header:
+# Connection: "close"
+
+# Print detailed latency statistics.
+# Type: boolean
+latency: False
+
+# Record a timeout if a response is not received within this amount of time.
+# Type: integer
+timeout: 5
diff --git a/resources/traffic_profiles/wrk/wrk-sf-2n-ethip4tcphttp-1u1c50con-rps.yaml b/resources/traffic_profiles/wrk/wrk-sf-2n-ethip4tcphttp-1u1c50con-rps.yaml
new file mode 100644
index 0000000000..939703638b
--- /dev/null
+++ b/resources/traffic_profiles/wrk/wrk-sf-2n-ethip4tcphttp-1u1c50con-rps.yaml
@@ -0,0 +1,47 @@
+# This is an example wrk traffic profile.
+
+# List of urls for requests. Each url is requested in a separate instance of
+# wrk.
+# Type: list
+urls:
+ # There must be a file (1kB - 1MB) requested but vpp does not support it.
+ - "http://192.168.10.2"
+
+# Index of the first CPU on the numa used to run wrk.
+# Type: integer
+first-cpu: 1
+
+# The number of cpus used for wrk. The number of cpus must be a
+# multiplication of the number of urls.
+# Type: integer
+cpus: 1
+
+# Duration of the test in seconds.
+# Type: integer
+duration: 30
+
+# Total number of threads to use.
+# Type: integer
+nr-of-threads: 1
+
+# Total number of HTTP connections to keep open with each thread handling
+# N = connections/threads.
+# Type: integer
+nr-of-connections: 50
+
+# Path to LuaJIT script.
+# Type: string
+# script: ""
+
+# HTTP header to add to request, e.g. "Connection: close".
+# Type: string (taken as it is) or dictionary
+# header:
+# Connection: "close"
+
+# Print detailed latency statistics.
+# Type: boolean
+latency: False
+
+# Record a timeout if a response is not received within this amount of time.
+# Type: integer
+timeout: 5
diff --git a/tests/vpp/perf/tcp/10ge2p1x520-ethip4tcphttp-httpserver.robot b/tests/vpp/perf/tcp/10ge2p1x520-ethip4tcphttp-httpserver.robot
new file mode 100644
index 0000000000..867c3177d5
--- /dev/null
+++ b/tests/vpp/perf/tcp/10ge2p1x520-ethip4tcphttp-httpserver.robot
@@ -0,0 +1,127 @@
+# Copyright (c) 2018 Cisco and/or its affiliates.
+# 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.
+
+*** Settings ***
+
+| Library | resources.tools.wrk.wrk
+| Resource | resources/libraries/robot/wrk/wrk_utils.robot
+| Resource | resources/libraries/robot/performance/performance_setup.robot
+| Resource | resources/libraries/robot/tcp/tcp_setup.robot
+| ...
+| Force Tags | 3_NODE_SINGLE_LINK_TOPO | PERFTEST | HW_ENV | HTTP | TCP
+| ...
+| Suite Setup | Set up 3-node performance topology with wrk and DUT's NIC model
+| ... | Intel-XL710
+| ...
+| Test Setup | Set up performance test
+| Test Teardown | Tear down performance test with wrk
+| ...
+| Documentation | *HTTP requests per seconds, connections per seconds and
+| ... | throughput measurement.*
+| ...
+| ... | *[Top] Network Topologies:* TG-DUT-TG 2-node topology
+| ... | with single link between nodes.
+| ... | *[Enc] Packet Encapsulations:* Eth-IPv4 for IPv4 routing.
+| ... | *[Cfg] DUT configuration:*
+| ... | *[Ver] TG verification:*
+| ... | *[Ref] Applicable standard specifications:*
+
+*** Keywords ***
+| Measure throughput or rps or cps
+| | [Arguments] | ${traffic_profile} | ${wt} | ${rxq} | ${test_type}
+| | ...
+| | Add '${wt}' worker threads and '${rxq}' rxqueues in 3-node single-link circular topology
+| | Add PCI devices to DUTs in 3-node single link topology
+| | ${duts}= | Get Matches | ${nodes} | DUT*
+| | :FOR | ${dut} | IN | @{duts}
+| | | Import Library | resources.libraries.python.VppConfigGenerator
+| | | ... | WITH NAME | ${dut}
+| | | Run keyword | ${dut}.Add TCP preallocated connections | 1000000
+| | | Run keyword | ${dut}.Add TCP preallocated half open connections | 1000000
+| | | Run keyword | ${dut}.Add session event queue length | 1000000
+| | | Run keyword | ${dut}.Add session preallocated sessions | 1000000
+| | | Run keyword | ${dut}.Add session v4 session table buckets | 500000
+| | | Run keyword | ${dut}.Add session v4 session table memory | 1g
+| | | Run keyword | ${dut}.Add session v4 halfopen table buckets | 2500000
+| | | Run keyword | ${dut}.Add session v4 halfopen table memory | 3g
+| | | Run keyword | ${dut}.Add session local endpoints table buckets | 2500000
+| | | Run keyword | ${dut}.Add session local endpoints table memory | 3g
+| | Apply startup configuration on all VPP DUTs
+| | Set up HTTP server on the VPP node | 192.168.10.2 | 24
+| | Run Keyword If | '${test_type}' == 'bw'
+| | ... | Measure throughput | ${traffic_profile}
+| | ... | ELSE IF | '${test_type}' == 'rps'
+| | ... | Measure requests per second | ${traffic_profile}
+| | ... | ELSE IF | '${test_type}' == 'cps'
+| | ... | Measure connections per second | ${traffic_profile}
+
+*** Test Cases ***
+| tc01-1t1c-ethip4tcphttp-httpserver-cps
+| | [Documentation]
+| | ... | Measure number of connections per second using wrk.
+| | ...
+| | [Tags] | 1T1C | TCP_CPS
+| | ...
+| | [Template] | Measure throughput or rps or cps
+| | traffic_profile=wrk-sf-2n-ethip4tcphttp-1u1c1con-cps | wt=1 | rxq=1
+| | ... | test_type=cps
+
+| tc02-2t2c-ethip4tcphttp-httpserver-cps
+| | [Documentation]
+| | ... | Measure number of connections per second using wrk.
+| | ...
+| | [Tags] | 2T2C | TCP_CPS
+| | ...
+| | [Template] | Measure throughput or rps or cps
+| | traffic_profile=wrk-sf-2n-ethip4tcphttp-1u1c1con-cps | wt=2 | rxq=1
+| | ... | test_type=cps
+
+| tc03-4t4c-ethip4tcphttp-httpserver-cps
+| | [Documentation]
+| | ... | Measure number of connections per second using wrk.
+| | ...
+| | [Tags] | 4T4C | TCP_CPS
+| | ...
+| | [Template] | Measure throughput or rps or cps
+| | traffic_profile=wrk-sf-2n-ethip4tcphttp-1u1c1con-cps | wt=4 | rxq=2
+| | ... | test_type=cps
+
+| tc04-1t1c-ethip4tcphttp-httpserver-rps
+| | [Documentation]
+| | ... | Measure and report number of requests per second using wrk.
+| | ...
+| | [Tags] | 1T1C | TCP_RPS
+| | ...
+| | [Template] | Measure throughput or rps or cps
+| | traffic_profile=wrk-sf-2n-ethip4tcphttp-1u1c50con-rps | wt=1 | rxq=1
+| | ... | test_type=rps
+
+| tc05-2t2c-ethip4tcphttp-httpserver-rps
+| | [Documentation]
+| | ... | Measure and report number of requests per second using wrk.
+| | ...
+| | [Tags] | 2T2C | TCP_RPS
+| | ...
+| | [Template] | Measure throughput or rps or cps
+| | traffic_profile=wrk-sf-2n-ethip4tcphttp-1u1c50con-rps | wt=2 | rxq=1
+| | ... | test_type=rps
+
+| tc06-4t4c-ethip4tcphttp-httpserver-rps
+| | [Documentation]
+| | ... | Measure and report number of requests per second using wrk.
+| | ...
+| | [Tags] | 4T4C | TCP_RPS
+| | ...
+| | [Template] | Measure throughput or rps or cps
+| | traffic_profile=wrk-sf-2n-ethip4tcphttp-1u1c50con-rps | wt=4 | rxq=2
+| | ... | test_type=rps