blob: a388b7ea1b5c43eca325bdec0af77968c2b90ed5 [file] [log] [blame]
#!/usr/bin/python
#
# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import logging, numpy, os, shutil, socket
import struct, subprocess, tempfile, time
from autotest_lib.client.bin import utils, test
from autotest_lib.client.common_lib import error
def selection_sequential(cur_index, length):
"""
Iterates over processes sequentially. This should cause worst-case
behavior for an LRU swap policy.
@param cur_index: Index of current hog (if sequential)
@param length: Number of hog processes
"""
return cur_index
def selection_exp(cur_index, length):
"""
Iterates over processes randomly according to an exponential distribution.
Simulates preference for a few long-lived tabs over others.
@param cur_index: Index of current hog (if sequential)
@param length: Number of hog processes
"""
# Discard any values greater than the length of the array.
# Inelegant, but necessary. Otherwise, the distribution will be skewed.
exp_value = length
while exp_value >= length:
# Mean is index 4 (weights the first 4 indexes the most).
exp_value = numpy.random.geometric(0.25) - 1
return int(exp_value)
def selection_uniform(cur_index, length):
"""
Iterates over processes randomly according to a uniform distribution.
@param cur_index: Index of current hog (if sequential)
@param length: Number of hog processes
"""
return numpy.random.randint(0, length)
# The available selection functions to use.
selection_funcs = {'sequential': selection_sequential,
'exponential': selection_exp,
'uniform': selection_uniform}
def get_selection_funcs(selections):
"""
Returns the selection functions listed by their names in 'selections'.
@param selections: List of strings, where each string is a key for a
selection function
"""
return {
k: selection_funcs[k]
for k in selection_funcs
if k in selections
}
def reset_zram():
"""
Resets zram, clearing all swap space.
"""
swapoff_timeout = 60
zram_device = 'zram0'
zram_device_path = os.path.join('/dev', zram_device)
reset_path = os.path.join('/sys/block', zram_device, 'reset')
disksize_path = os.path.join('/sys/block', zram_device, 'disksize')
disksize = utils.read_one_line(disksize_path)
# swapoff is prone to hanging, especially after heavy swap usage, so
# time out swapoff if it takes too long.
ret = utils.system('swapoff ' + zram_device_path,
timeout=swapoff_timeout, ignore_status=True)
if ret != 0:
raise error.TestFail('Could not reset zram - swapoff failed.')
# Sleep to avoid "device busy" errors.
time.sleep(1)
utils.write_one_line(reset_path, '1')
time.sleep(1)
utils.write_one_line(disksize_path, disksize)
utils.system('mkswap ' + zram_device_path)
utils.system('swapon ' + zram_device_path)
swap_reset_funcs = {'zram': reset_zram}
class platform_CompressedSwapPerf(test.test):
"""Runs basic performance benchmarks on compressed swap.
Launches a number of "hog" processes that can be told to "balloon"
(allocating a specified amount of memory in 1 MiB chunks) and can
also be "poked", which reads from and writes to random places in memory
to force swapping in and out. Hog processes report back statistics on how
long a "poke" took (real and CPU time) and number of page faults.
"""
version = 1
executable = 'hog'
swap_enable_file = '/home/chronos/.swap_enabled'
swap_disksize_file = '/sys/block/zram0/disksize'
CMD_POKE = 1
CMD_BALLOON = 2
CMD_EXIT = 3
CMD_FORMAT = "=L"
CMD_FORMAT_SIZE = struct.calcsize(CMD_FORMAT)
RESULT_FORMAT = "=QQQQ"
RESULT_FORMAT_SIZE = struct.calcsize(RESULT_FORMAT)
def setup(self):
"""
Compiles the hog program.
"""
os.chdir(self.srcdir)
utils.make(self.executable)
def report_stat(self, units, swap_target, selection, metric, stat, value):
"""
Reports a single performance statistic. This function puts the supplied
args into an autotest-approved format.
@param units: String describing units of the statistic
@param swap_target: Current swap target, 0.0 <= swap_target < 1.0
@param selection: Name of selection function to report for
@param metric: Name of the metric that is being reported
@param stat: Name of the statistic (e.g. median, 99th percentile)
@param value: Actual floating-point value
"""
swap_target_str = '%.2f' % swap_target
perfkey_name_list = [ units, 'swap', swap_target_str,
selection, metric, stat ]
# Filter out any args that evaluate to false.
perfkey_name_list = filter(None, perfkey_name_list)
perf_key = '_'.join(perfkey_name_list)
self.write_perf_keyval({perf_key: value})
def report_stats(self, units, swap_target, selection, metric, values):
"""
Reports interesting statistics from a list of recorded values.
@param units: String describing units of the statistic
@param swap_target: Current swap target
@param selection: Name of current selection function
@param metric: Name of the metric that is being reported
@param values: List of floating point measurements for this metric
"""
if not values:
logging.info('Cannot report empty list!')
return
values = sorted(values)
mean = float(sum(values)) / len(values)
median = values[int(0.5*len(values))]
percentile_95 = values[int(0.95*len(values))]
percentile_99 = values[int(0.99*len(values))]
self.report_stat(units, swap_target, selection, metric, 'mean', mean)
self.report_stat(units, swap_target, selection,
metric, 'median', median)
self.report_stat(units, swap_target, selection,
metric, '95th_percentile', percentile_95)
self.report_stat(units, swap_target, selection,
metric, '99th_percentile', percentile_99)
def sample_memory_state(self):
"""
Samples memory info from /proc/meminfo and use that to calculate swap
usage and total memory usage, adjusted for double-counting swap space.
"""
self.mem_total = utils.read_from_meminfo('MemTotal')
self.swap_total = utils.read_from_meminfo('SwapTotal')
self.mem_free = utils.read_from_meminfo('MemFree')
self.swap_free = utils.read_from_meminfo('SwapFree')
self.swap_used = self.swap_total - self.swap_free
used_phys_memory = self.mem_total - self.mem_free
# Get zram's actual compressed size and convert to KiB.
swap_phys_size = utils.read_one_line('/sys/block/zram0/compr_data_size')
swap_phys_size = int(swap_phys_size) / 1024
self.total_usage = used_phys_memory - swap_phys_size + self.swap_used
self.usage_ratio = float(self.swap_used) / self.swap_total
def send_poke(self, hog_sock):
"""Pokes a hog process.
Poking a hog causes it to simulate activity and report back on
the same socket.
@param hog_sock: An open socket to the hog process
"""
hog_sock.send(struct.pack(self.CMD_FORMAT, self.CMD_POKE))
def send_balloon(self, hog_sock, alloc_mb):
"""Tells a hog process to allocate more memory.
@param hog_sock: An open socket to the hog process
@param alloc_mb: Amount of memory to allocate, in MiB
"""
hog_sock.send(struct.pack(self.CMD_FORMAT, self.CMD_BALLOON))
hog_sock.send(struct.pack(self.CMD_FORMAT, alloc_mb))
def send_exit(self, hog_sock):
"""Tells a hog process to exit and closes the socket.
@param hog_sock: An open socket to the hog process
"""
hog_sock.send(struct.pack(self.CMD_FORMAT, self.CMD_EXIT))
hog_sock.shutdown(socket.SHUT_RDWR)
hog_sock.close()
def recv_poke_results(self, hog_sock):
"""Returns the results from poking a hog as a tuple.
@param hog_sock: An open socket to the hog process
@return: A tuple (wall_time, user_time, sys_time, fault_count)
"""
try:
result = hog_sock.recv(self.RESULT_FORMAT_SIZE)
if len(result) != self.RESULT_FORMAT_SIZE:
logging.info("incorrect result, len %d",
len(result))
else:
result_unpacked = struct.unpack(self.RESULT_FORMAT, result)
wall_time = result_unpacked[0]
user_time = result_unpacked[1]
sys_time = result_unpacked[2]
fault_count = result_unpacked[3]
return (wall_time, user_time, sys_time, fault_count)
except socket.error:
logging.info('Hog died while touching memory')
def recv_balloon_results(self, hog_sock, alloc_mb):
"""Receives a balloon response from a hog.
If a hog succeeds in allocating more memory, it will respond on its
socket with the original allocation size.
@param hog_sock: An open socket to the hog process
@param alloc_mb: Amount of memory to allocate, in MiB
@raise TestFail: Fails if hog could not allocate memory, or if
there is a communication problem.
"""
balloon_result = hog_sock.recv(self.CMD_FORMAT_SIZE)
if len(balloon_result) != self.CMD_FORMAT_SIZE:
return False
balloon_result_unpack = struct.unpack(self.CMD_FORMAT, balloon_result)
return balloon_result_unpack == alloc_mb
def run_single_test(self, compression_factor, num_procs, cycles,
swap_target, switch_delay, temp_dir, selections):
"""
Runs the benchmark for a single swap target usage.
@param compression_factor: Compression factor (int)
example: compression_factor=3 is 1:3 ratio
@param num_procs: Number of hog processes to use
@param cycles: Number of iterations over hogs list for a given swap lvl
@param swap_target: Floating point value of target swap usage
@param switch_delay: Number of seconds to wait between poking hogs
@param temp_dir: Path of the temporary directory to use
@param selections: List of selection function names
"""
# Get initial memory state.
self.sample_memory_state()
swap_target_usage = swap_target * self.swap_total
# usage_target is our estimate on the amount of memory that needs to
# be allocated to reach our target swap usage.
swap_target_phys = swap_target_usage / compression_factor
usage_target = self.mem_free - swap_target_phys + swap_target_usage
hogs = []
paths = []
sockets = []
cmd = [ os.path.join(self.srcdir, self.executable) ]
# Launch hog processes.
while len(hogs) < num_procs:
socket_path = os.path.join(temp_dir, str(len(hogs)))
paths.append(socket_path)
launch_cmd = list(cmd)
launch_cmd.append(socket_path)
launch_cmd.append(str(compression_factor))
p = subprocess.Popen(launch_cmd)
utils.write_one_line('/proc/%d/oom_score_adj' % p.pid, '15')
hogs.append(p)
# Open sockets to hog processes, waiting for them to bind first.
time.sleep(5)
for socket_path in paths:
hog_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sockets.append(hog_sock)
hog_sock.connect(socket_path)
# Allocate conservatively until we reach our target.
while self.usage_ratio <= swap_target:
free_per_hog = (usage_target - self.total_usage) / len(hogs)
alloc_per_hog_mb = int(0.80 * free_per_hog) / 1024
if alloc_per_hog_mb <= 0:
alloc_per_hog_mb = 1
# Send balloon command.
for hog_sock in sockets:
self.send_balloon(hog_sock, alloc_per_hog_mb)
# Wait until all hogs report back.
for hog_sock in sockets:
self.recv_balloon_results(hog_sock, alloc_per_hog_mb)
# We need to sample memory and swap usage again.
self.sample_memory_state()
# Once memory is allocated, report how close we got to the swap target.
self.report_stat('percent', swap_target, None,
'usage', 'value', self.usage_ratio)
# Run tests by sending "touch memory" command to hogs.
for f_name, f in get_selection_funcs(selections).iteritems():
result_list = []
for count in range(cycles):
for i in range(len(hogs)):
selection = f(i, len(hogs))
hog_sock = sockets[selection]
retcode = hogs[selection].poll()
# Ensure that the hog is not dead.
if retcode is None:
# Delay between switching "tabs".
if switch_delay > 0.0:
time.sleep(switch_delay)
self.send_poke(hog_sock)
result = self.recv_poke_results(hog_sock)
if result:
result_list.append(result)
else:
logging.info("Hog died unexpectedly; continuing")
# Convert from list of tuples (rtime, utime, stime, faults) to
# a list of rtimes, a list of utimes, etc.
results_unzipped = [list(x) for x in zip(*result_list)]
wall_times = results_unzipped[0]
user_times = results_unzipped[1]
sys_times = results_unzipped[2]
fault_counts = results_unzipped[3]
# Calculate average time to service a fault for each sample.
us_per_fault_list = []
for i in range(len(sys_times)):
if fault_counts[i] == 0.0:
us_per_fault_list.append(0.0)
else:
us_per_fault_list.append(sys_times[i] * 1000.0 /
fault_counts[i])
self.report_stats('ms', swap_target, f_name, 'rtime', wall_times)
self.report_stats('ms', swap_target, f_name, 'utime', user_times)
self.report_stats('ms', swap_target, f_name, 'stime', sys_times)
self.report_stats('faults', swap_target, f_name, 'faults',
fault_counts)
self.report_stats('us_fault', swap_target, f_name, 'fault_time',
us_per_fault_list)
# Send exit message to all hogs.
for hog_sock in sockets:
self.send_exit(hog_sock)
time.sleep(1)
# If hogs didn't exit normally, kill them.
for hog in hogs:
retcode = hog.poll()
if retcode is None:
logging.debug("killing all remaining hogs")
utils.system("killall -TERM hog")
# Wait to ensure hogs have died before continuing.
time.sleep(5)
break
def run_once(self, compression_factor=3, num_procs=50, cycles=20,
selections=None, swap_targets=None, switch_delay=0.0):
if selections is None:
selections = ['sequential', 'uniform', 'exponential']
if swap_targets is None:
swap_targets = [0.00, 0.25, 0.50, 0.75, 0.95]
swaptotal = utils.read_from_meminfo('SwapTotal')
# Check for proper swap space configuration.
# If the swap enable file says "0", swap.conf does not create swap.
if os.path.exists(self.swap_enable_file):
enable_size = utils.read_one_line(self.swap_enable_file)
else:
enable_size = "nonexistent" # implies nonzero
if enable_size == "0":
if swaptotal != 0:
raise error.TestFail('The swap enable file said 0, but'
' swap was still enabled for %d.' %
swaptotal)
logging.info('Swap enable (0), swap disabled.')
else:
# Rather than parsing swap.conf logic to calculate a size,
# use the value it writes to /sys/block/zram0/disksize.
if not os.path.exists(self.swap_disksize_file):
raise error.TestFail('The %s swap enable file should have'
' caused zram to load, but %s was'
' not found.' %
(enable_size, self.swap_disksize_file))
disksize = utils.read_one_line(self.swap_disksize_file)
swaprequested = int(disksize) / 1000
if (swaptotal < swaprequested * 0.9 or
swaptotal > swaprequested * 1.1):
raise error.TestFail('Our swap of %d K is not within 10%%'
' of the %d K we requested.' %
(swaptotal, swaprequested))
logging.info('Swap enable (%s), requested %d, total %d',
enable_size, swaprequested, swaptotal)
# We should try to autodetect this if we add other swap methods.
swap_method = 'zram'
for swap_target in swap_targets:
logging.info('swap_target is %f', swap_target)
temp_dir = tempfile.mkdtemp()
try:
# Reset swap space to make sure nothing leaks between runs.
swap_reset = swap_reset_funcs[swap_method]
swap_reset()
self.run_single_test(compression_factor, num_procs, cycles,
swap_target, switch_delay, temp_dir,
selections)
except socket.error:
logging.debug('swap target %f failed; oom killer?', swap_target)
shutil.rmtree(temp_dir)