| #!/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) |
| |