Skip to content

Commit 175246e

Browse files
ioanachircadianpopa
authored andcommitted
tests: add cpu load test for high rx
This is a regression test for #1444 Signed-off-by: Ioana Chirca <[email protected]>
1 parent 1374b26 commit 175246e

File tree

3 files changed

+192
-0
lines changed

3 files changed

+192
-0
lines changed

tests/framework/microvm.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
from retry import retry
1919

20+
import host_tools.cpu_load as cpu_tools
2021
import host_tools.memory as mem_tools
2122
import host_tools.network as net_tools
2223

@@ -119,6 +120,10 @@ def __init__(
119120
else:
120121
self._memory_events_queue = None
121122

123+
# Cpu load monitoring has to be explicitly enabled using
124+
# the `enable_cpu_load_monitor` method.
125+
self._cpu_load_monitor = None
126+
122127
# External clone/exec tool, because Python can't into clone
123128
self.bin_cloner_path = bin_cloner_path
124129

@@ -138,6 +143,11 @@ def kill(self):
138143
raise mem_tools.MemoryUsageExceededException(
139144
self._memory_events_queue.get())
140145

146+
if self._cpu_load_monitor:
147+
self._cpu_load_monitor.signal_stop()
148+
self._cpu_load_monitor.join()
149+
self._cpu_load_monitor.check_samples()
150+
141151
@property
142152
def api_session(self):
143153
"""Return the api session associated with this microVM."""
@@ -215,6 +225,20 @@ def memory_events_queue(self, queue):
215225
"""Set the memory usage events queue."""
216226
self._memory_events_queue = queue
217227

228+
def enable_cpu_load_monitor(self, threshold):
229+
"""Enable the cpu load monitor."""
230+
process_pid = self.jailer_clone_pid
231+
# We want to monitor the emulation thread, which is currently
232+
# the first one created.
233+
# A possible improvement is to find it by name.
234+
thread_pid = self.jailer_clone_pid
235+
self._cpu_load_monitor = cpu_tools.CpuLoadMonitor(
236+
process_pid,
237+
thread_pid,
238+
threshold
239+
)
240+
self._cpu_load_monitor.start()
241+
218242
def create_jailed_resource(self, path, create_jail=False):
219243
"""Create a hard link to some resource inside this microvm."""
220244
return self.jailer.jailed_path(path, create=True,

tests/host_tools/cpu_load.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Utilities for measuring cpu utilisation for a process."""
4+
import time
5+
6+
from subprocess import run, CalledProcessError, PIPE
7+
from threading import Thread
8+
9+
# /proc/<pid>/stat output taken from
10+
# https://www.man7.org/linux/man-pages/man5/proc.5.html
11+
STAT_UTIME_IDX = 13
12+
STAT_STIME_IDX = 14
13+
STAT_STARTTIME_IDX = 21
14+
15+
16+
class CpuLoadExceededException(Exception):
17+
"""A custom exception containing details on excessive cpu load."""
18+
19+
def __init__(self, cpu_load_samples, threshold):
20+
"""Compose the error message containing the cpu load details."""
21+
super(CpuLoadExceededException, self).__init__(
22+
'Cpu load samples {} exceeded maximum threshold {}.\n'
23+
.format(cpu_load_samples, threshold)
24+
)
25+
26+
27+
class CpuLoadMonitor(Thread):
28+
"""Class to represent a cpu load monitor for a thread."""
29+
30+
CPU_LOAD_SAMPLES_TIMEOUT_S = 1
31+
32+
def __init__(
33+
self,
34+
process_pid,
35+
thread_pid,
36+
threshold
37+
):
38+
"""Set up monitor attributes."""
39+
Thread.__init__(self)
40+
self._process_pid = process_pid
41+
self._thread_pid = thread_pid
42+
self._cpu_load_samples = []
43+
self._threshold = threshold
44+
self._should_stop = False
45+
46+
@property
47+
def process_pid(self):
48+
"""Get the process pid."""
49+
return self._process_pid
50+
51+
@property
52+
def thread_pid(self):
53+
"""Get the thread pid."""
54+
return self._thread_pid
55+
56+
@property
57+
def threshold(self):
58+
"""Get the cpu load threshold."""
59+
return self._threshold
60+
61+
@property
62+
def cpu_load_samples(self):
63+
"""Get the cpu load samples."""
64+
return self._cpu_load_samples
65+
66+
def signal_stop(self):
67+
"""Signal that the thread should stop."""
68+
self._should_stop = True
69+
70+
def run(self):
71+
"""Thread for monitoring cpu load of some pid.
72+
73+
`/proc/<process pid>/task/<thread pid>/stat` is used to compute
74+
the cpu load, which is then added to the list.
75+
It is up to the caller to check the queue.
76+
"""
77+
clock_ticks_cmd = 'getconf CLK_TCK'
78+
try:
79+
stdout = run(
80+
clock_ticks_cmd,
81+
shell=True,
82+
check=True,
83+
stdout=PIPE
84+
).stdout.decode('utf-8')
85+
except CalledProcessError:
86+
return
87+
try:
88+
clock_ticks = int(stdout.strip("\n"))
89+
except ValueError:
90+
return
91+
92+
while not self._should_stop:
93+
try:
94+
with open('/proc/uptime') as uptime_file:
95+
uptime = uptime_file.readline().strip("\n").split()[0]
96+
97+
with open('/proc/{pid}/task/{tid}/stat'.format(
98+
pid=self.process_pid,
99+
tid=self.thread_pid)
100+
) as stat_file:
101+
stat = stat_file.readline().strip("\n").split()
102+
except IOError:
103+
break
104+
105+
try:
106+
uptime = float(uptime)
107+
utime = int(stat[STAT_UTIME_IDX])
108+
stime = int(stat[STAT_STIME_IDX])
109+
starttime = int(stat[STAT_STARTTIME_IDX])
110+
except ValueError:
111+
break
112+
113+
total_time = utime + stime
114+
seconds = uptime - starttime / clock_ticks
115+
cpu_load = (total_time * 100 / clock_ticks) / seconds
116+
117+
if cpu_load > self.threshold:
118+
self.cpu_load_samples.append(cpu_load)
119+
120+
time.sleep(self.CPU_LOAD_SAMPLES_TIMEOUT_S)
121+
122+
def check_samples(self):
123+
"""Check that there are no samples above the threshold."""
124+
if len(self.cpu_load_samples) > 0:
125+
raise CpuLoadExceededException(
126+
self._cpu_load_samples, self._threshold)

tests/integration_tests/functional/test_rate_limiter.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,48 @@ def test_rx_rate_limiting(test_microvm_with_ssh, network_config):
149149
_check_rx_rate_limit_patch(test_microvm, guest_ips)
150150

151151

152+
def test_rx_rate_limiting_cpu_load(test_microvm_with_ssh, network_config):
153+
"""Run iperf rx with rate limiting; verify cpu load is below threshold."""
154+
test_microvm = test_microvm_with_ssh
155+
test_microvm.spawn()
156+
157+
test_microvm.basic_config()
158+
159+
# Enable monitor that checks if the cpu load is over the threshold.
160+
# After multiple runs, the average value for the cpu load
161+
# seems to be around 10%. Setting the threshold a little
162+
# higher to skip false positives.
163+
threshold = 20
164+
test_microvm.enable_cpu_load_monitor(threshold)
165+
166+
# Create interface with aggressive rate limiting enabled.
167+
rx_rate_limiter_no_burst = {
168+
'bandwidth': {
169+
'size': 65536, # 64KBytes
170+
'refill_time': 1000 # 1s
171+
}
172+
}
173+
_tap, _host_ip, guest_ip = test_microvm.ssh_network_config(
174+
network_config,
175+
'1',
176+
rx_rate_limiter=rx_rate_limiter_no_burst
177+
)
178+
179+
test_microvm.start()
180+
181+
# Start iperf server on guest.
182+
_start_iperf_on_guest(test_microvm, guest_ip)
183+
184+
# Run iperf client sending UDP traffic.
185+
iperf_cmd = '{} {} -u -c {} -b 1000000000 -t{} -f KBytes'.format(
186+
test_microvm.jailer.netns_cmd_prefix(),
187+
IPERF_BINARY,
188+
guest_ip,
189+
IPERF_TRANSMIT_TIME * 5
190+
)
191+
_iperf_out = _run_local_iperf(iperf_cmd)
192+
193+
152194
def _check_tx_rate_limiting(test_microvm, guest_ips, host_ips):
153195
"""Check that the transmit rate is within expectations."""
154196
# Start iperf on the host as this is the tx rate limiting test.

0 commit comments

Comments
 (0)