1 #!/usr/bin/python 2 # 3 # Copyright (c) 2013 The Chromium OS Authors. All rights reserved. 4 # Use of this source code is governed by a BSD-style license that can be 5 # found in the LICENSE file. 6 7 import logging, numpy, os, shutil, socket 8 import struct, subprocess, tempfile, time 9 10 from autotest_lib.client.bin import utils, test 11 from autotest_lib.client.common_lib import error 12 13 def selection_sequential(cur_index, length): 14 """ 15 Iterates over processes sequentially. This should cause worst-case 16 behavior for an LRU swap policy. 17 18 @param cur_index: Index of current hog (if sequential) 19 @param length: Number of hog processes 20 """ 21 return cur_index 22 23 def selection_exp(cur_index, length): 24 """ 25 Iterates over processes randomly according to an exponential distribution. 26 Simulates preference for a few long-lived tabs over others. 27 28 @param cur_index: Index of current hog (if sequential) 29 @param length: Number of hog processes 30 """ 31 32 # Discard any values greater than the length of the array. 33 # Inelegant, but necessary. Otherwise, the distribution will be skewed. 34 exp_value = length 35 while exp_value >= length: 36 # Mean is index 4 (weights the first 4 indexes the most). 37 exp_value = numpy.random.geometric(0.25) - 1 38 return int(exp_value) 39 40 def selection_uniform(cur_index, length): 41 """ 42 Iterates over processes randomly according to a uniform distribution. 43 44 @param cur_index: Index of current hog (if sequential) 45 @param length: Number of hog processes 46 """ 47 return numpy.random.randint(0, length) 48 49 # The available selection functions to use. 50 selection_funcs = {'sequential': selection_sequential, 51 'exponential': selection_exp, 52 'uniform': selection_uniform} 53 54 def get_selection_funcs(selections): 55 """ 56 Returns the selection functions listed by their names in 'selections'. 57 58 @param selections: List of strings, where each string is a key for a 59 selection function 60 """ 61 return { 62 k: selection_funcs[k] 63 for k in selection_funcs 64 if k in selections 65 } 66 67 def reset_zram(): 68 """ 69 Resets zram, clearing all swap space. 70 """ 71 swapoff_timeout = 60 72 zram_device = 'zram0' 73 zram_device_path = os.path.join('/dev', zram_device) 74 reset_path = os.path.join('/sys/block', zram_device, 'reset') 75 disksize_path = os.path.join('/sys/block', zram_device, 'disksize') 76 77 disksize = utils.read_one_line(disksize_path) 78 79 # swapoff is prone to hanging, especially after heavy swap usage, so 80 # time out swapoff if it takes too long. 81 ret = utils.system('swapoff ' + zram_device_path, 82 timeout=swapoff_timeout, ignore_status=True) 83 84 if ret != 0: 85 raise error.TestFail('Could not reset zram - swapoff failed.') 86 87 # Sleep to avoid "device busy" errors. 88 time.sleep(1) 89 utils.write_one_line(reset_path, '1') 90 time.sleep(1) 91 utils.write_one_line(disksize_path, disksize) 92 utils.system('mkswap ' + zram_device_path) 93 utils.system('swapon ' + zram_device_path) 94 95 swap_reset_funcs = {'zram': reset_zram} 96 97 class platform_CompressedSwapPerf(test.test): 98 """Runs basic performance benchmarks on compressed swap. 99 100 Launches a number of "hog" processes that can be told to "balloon" 101 (allocating a specified amount of memory in 1 MiB chunks) and can 102 also be "poked", which reads from and writes to random places in memory 103 to force swapping in and out. Hog processes report back statistics on how 104 long a "poke" took (real and CPU time) and number of page faults. 105 """ 106 version = 1 107 executable = 'hog' 108 swap_enable_file = '/home/chronos/.swap_enabled' 109 swap_disksize_file = '/sys/block/zram0/disksize' 110 111 CMD_POKE = 1 112 CMD_BALLOON = 2 113 CMD_EXIT = 3 114 115 CMD_FORMAT = "=L" 116 CMD_FORMAT_SIZE = struct.calcsize(CMD_FORMAT) 117 118 RESULT_FORMAT = "=QQQQ" 119 RESULT_FORMAT_SIZE = struct.calcsize(RESULT_FORMAT) 120 121 def setup(self): 122 """ 123 Compiles the hog program. 124 """ 125 os.chdir(self.srcdir) 126 utils.make(self.executable) 127 128 def report_stat(self, units, swap_target, selection, metric, stat, value): 129 """ 130 Reports a single performance statistic. This function puts the supplied 131 args into an autotest-approved format. 132 133 @param units: String describing units of the statistic 134 @param swap_target: Current swap target, 0.0 <= swap_target < 1.0 135 @param selection: Name of selection function to report for 136 @param metric: Name of the metric that is being reported 137 @param stat: Name of the statistic (e.g. median, 99th percentile) 138 @param value: Actual floating-point value 139 """ 140 swap_target_str = '%.2f' % swap_target 141 perfkey_name_list = [ units, 'swap', swap_target_str, 142 selection, metric, stat ] 143 144 # Filter out any args that evaluate to false. 145 perfkey_name_list = filter(None, perfkey_name_list) 146 perf_key = '_'.join(perfkey_name_list) 147 self.write_perf_keyval({perf_key: value}) 148 149 def report_stats(self, units, swap_target, selection, metric, values): 150 """ 151 Reports interesting statistics from a list of recorded values. 152 153 @param units: String describing units of the statistic 154 @param swap_target: Current swap target 155 @param selection: Name of current selection function 156 @param metric: Name of the metric that is being reported 157 @param values: List of floating point measurements for this metric 158 """ 159 if not values: 160 logging.info('Cannot report empty list!') 161 return 162 163 values = sorted(values) 164 mean = float(sum(values)) / len(values) 165 median = values[int(0.5*len(values))] 166 percentile_95 = values[int(0.95*len(values))] 167 percentile_99 = values[int(0.99*len(values))] 168 169 self.report_stat(units, swap_target, selection, metric, 'mean', mean) 170 self.report_stat(units, swap_target, selection, 171 metric, 'median', median) 172 self.report_stat(units, swap_target, selection, 173 metric, '95th_percentile', percentile_95) 174 self.report_stat(units, swap_target, selection, 175 metric, '99th_percentile', percentile_99) 176 177 def sample_memory_state(self): 178 """ 179 Samples memory info from /proc/meminfo and use that to calculate swap 180 usage and total memory usage, adjusted for double-counting swap space. 181 """ 182 self.mem_total = utils.read_from_meminfo('MemTotal') 183 self.swap_total = utils.read_from_meminfo('SwapTotal') 184 self.mem_free = utils.read_from_meminfo('MemFree') 185 self.swap_free = utils.read_from_meminfo('SwapFree') 186 self.swap_used = self.swap_total - self.swap_free 187 188 used_phys_memory = self.mem_total - self.mem_free 189 190 # Get zram's actual compressed size and convert to KiB. 191 swap_phys_size = utils.read_one_line('/sys/block/zram0/compr_data_size') 192 swap_phys_size = int(swap_phys_size) / 1024 193 194 self.total_usage = used_phys_memory - swap_phys_size + self.swap_used 195 self.usage_ratio = float(self.swap_used) / self.swap_total 196 197 def send_poke(self, hog_sock): 198 """Pokes a hog process. 199 Poking a hog causes it to simulate activity and report back on 200 the same socket. 201 202 @param hog_sock: An open socket to the hog process 203 """ 204 hog_sock.send(struct.pack(self.CMD_FORMAT, self.CMD_POKE)) 205 206 def send_balloon(self, hog_sock, alloc_mb): 207 """Tells a hog process to allocate more memory. 208 209 @param hog_sock: An open socket to the hog process 210 @param alloc_mb: Amount of memory to allocate, in MiB 211 """ 212 hog_sock.send(struct.pack(self.CMD_FORMAT, self.CMD_BALLOON)) 213 hog_sock.send(struct.pack(self.CMD_FORMAT, alloc_mb)) 214 215 def send_exit(self, hog_sock): 216 """Tells a hog process to exit and closes the socket. 217 218 @param hog_sock: An open socket to the hog process 219 """ 220 hog_sock.send(struct.pack(self.CMD_FORMAT, self.CMD_EXIT)) 221 hog_sock.shutdown(socket.SHUT_RDWR) 222 hog_sock.close() 223 224 def recv_poke_results(self, hog_sock): 225 """Returns the results from poking a hog as a tuple. 226 227 @param hog_sock: An open socket to the hog process 228 @return: A tuple (wall_time, user_time, sys_time, fault_count) 229 """ 230 try: 231 result = hog_sock.recv(self.RESULT_FORMAT_SIZE) 232 if len(result) != self.RESULT_FORMAT_SIZE: 233 logging.info("incorrect result, len %d", 234 len(result)) 235 else: 236 result_unpacked = struct.unpack(self.RESULT_FORMAT, result) 237 wall_time = result_unpacked[0] 238 user_time = result_unpacked[1] 239 sys_time = result_unpacked[2] 240 fault_count = result_unpacked[3] 241 242 return (wall_time, user_time, sys_time, fault_count) 243 except socket.error: 244 logging.info('Hog died while touching memory') 245 246 247 248 def recv_balloon_results(self, hog_sock, alloc_mb): 249 """Receives a balloon response from a hog. 250 If a hog succeeds in allocating more memory, it will respond on its 251 socket with the original allocation size. 252 253 @param hog_sock: An open socket to the hog process 254 @param alloc_mb: Amount of memory to allocate, in MiB 255 @raise TestFail: Fails if hog could not allocate memory, or if 256 there is a communication problem. 257 """ 258 balloon_result = hog_sock.recv(self.CMD_FORMAT_SIZE) 259 if len(balloon_result) != self.CMD_FORMAT_SIZE: 260 return False 261 262 balloon_result_unpack = struct.unpack(self.CMD_FORMAT, balloon_result) 263 264 return balloon_result_unpack == alloc_mb 265 266 def run_single_test(self, compression_factor, num_procs, cycles, 267 swap_target, switch_delay, temp_dir, selections): 268 """ 269 Runs the benchmark for a single swap target usage. 270 271 @param compression_factor: Compression factor (int) 272 example: compression_factor=3 is 1:3 ratio 273 @param num_procs: Number of hog processes to use 274 @param cycles: Number of iterations over hogs list for a given swap lvl 275 @param swap_target: Floating point value of target swap usage 276 @param switch_delay: Number of seconds to wait between poking hogs 277 @param temp_dir: Path of the temporary directory to use 278 @param selections: List of selection function names 279 """ 280 # Get initial memory state. 281 self.sample_memory_state() 282 swap_target_usage = swap_target * self.swap_total 283 284 # usage_target is our estimate on the amount of memory that needs to 285 # be allocated to reach our target swap usage. 286 swap_target_phys = swap_target_usage / compression_factor 287 usage_target = self.mem_free - swap_target_phys + swap_target_usage 288 289 hogs = [] 290 paths = [] 291 sockets = [] 292 cmd = [ os.path.join(self.srcdir, self.executable) ] 293 294 # Launch hog processes. 295 while len(hogs) < num_procs: 296 socket_path = os.path.join(temp_dir, str(len(hogs))) 297 paths.append(socket_path) 298 launch_cmd = list(cmd) 299 launch_cmd.append(socket_path) 300 launch_cmd.append(str(compression_factor)) 301 p = subprocess.Popen(launch_cmd) 302 utils.write_one_line('/proc/%d/oom_score_adj' % p.pid, '15') 303 hogs.append(p) 304 305 # Open sockets to hog processes, waiting for them to bind first. 306 time.sleep(5) 307 for socket_path in paths: 308 hog_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 309 sockets.append(hog_sock) 310 hog_sock.connect(socket_path) 311 312 # Allocate conservatively until we reach our target. 313 while self.usage_ratio <= swap_target: 314 free_per_hog = (usage_target - self.total_usage) / len(hogs) 315 alloc_per_hog_mb = int(0.80 * free_per_hog) / 1024 316 if alloc_per_hog_mb <= 0: 317 alloc_per_hog_mb = 1 318 319 # Send balloon command. 320 for hog_sock in sockets: 321 self.send_balloon(hog_sock, alloc_per_hog_mb) 322 323 # Wait until all hogs report back. 324 for hog_sock in sockets: 325 self.recv_balloon_results(hog_sock, alloc_per_hog_mb) 326 327 # We need to sample memory and swap usage again. 328 self.sample_memory_state() 329 330 # Once memory is allocated, report how close we got to the swap target. 331 self.report_stat('percent', swap_target, None, 332 'usage', 'value', self.usage_ratio) 333 334 # Run tests by sending "touch memory" command to hogs. 335 for f_name, f in get_selection_funcs(selections).iteritems(): 336 result_list = [] 337 338 for count in range(cycles): 339 for i in range(len(hogs)): 340 selection = f(i, len(hogs)) 341 hog_sock = sockets[selection] 342 retcode = hogs[selection].poll() 343 344 # Ensure that the hog is not dead. 345 if retcode is None: 346 # Delay between switching "tabs". 347 if switch_delay > 0.0: 348 time.sleep(switch_delay) 349 350 self.send_poke(hog_sock) 351 352 result = self.recv_poke_results(hog_sock) 353 if result: 354 result_list.append(result) 355 else: 356 logging.info("Hog died unexpectedly; continuing") 357 358 # Convert from list of tuples (rtime, utime, stime, faults) to 359 # a list of rtimes, a list of utimes, etc. 360 results_unzipped = [list(x) for x in zip(*result_list)] 361 wall_times = results_unzipped[0] 362 user_times = results_unzipped[1] 363 sys_times = results_unzipped[2] 364 fault_counts = results_unzipped[3] 365 366 # Calculate average time to service a fault for each sample. 367 us_per_fault_list = [] 368 for i in range(len(sys_times)): 369 if fault_counts[i] == 0.0: 370 us_per_fault_list.append(0.0) 371 else: 372 us_per_fault_list.append(sys_times[i] * 1000.0 / 373 fault_counts[i]) 374 375 self.report_stats('ms', swap_target, f_name, 'rtime', wall_times) 376 self.report_stats('ms', swap_target, f_name, 'utime', user_times) 377 self.report_stats('ms', swap_target, f_name, 'stime', sys_times) 378 self.report_stats('faults', swap_target, f_name, 'faults', 379 fault_counts) 380 self.report_stats('us_fault', swap_target, f_name, 'fault_time', 381 us_per_fault_list) 382 383 # Send exit message to all hogs. 384 for hog_sock in sockets: 385 self.send_exit(hog_sock) 386 387 time.sleep(1) 388 389 # If hogs didn't exit normally, kill them. 390 for hog in hogs: 391 retcode = hog.poll() 392 if retcode is None: 393 logging.debug("killing all remaining hogs") 394 utils.system("killall -TERM hog") 395 # Wait to ensure hogs have died before continuing. 396 time.sleep(5) 397 break 398 399 def run_once(self, compression_factor=3, num_procs=50, cycles=20, 400 selections=None, swap_targets=None, switch_delay=0.0): 401 if selections is None: 402 selections = ['sequential', 'uniform', 'exponential'] 403 if swap_targets is None: 404 swap_targets = [0.00, 0.25, 0.50, 0.75, 0.95] 405 406 swaptotal = utils.read_from_meminfo('SwapTotal') 407 408 # Check for proper swap space configuration. 409 # If the swap enable file says "0", swap.conf does not create swap. 410 if os.path.exists(self.swap_enable_file): 411 enable_size = utils.read_one_line(self.swap_enable_file) 412 else: 413 enable_size = "nonexistent" # implies nonzero 414 if enable_size == "0": 415 if swaptotal != 0: 416 raise error.TestFail('The swap enable file said 0, but' 417 ' swap was still enabled for %d.' % 418 swaptotal) 419 logging.info('Swap enable (0), swap disabled.') 420 else: 421 # Rather than parsing swap.conf logic to calculate a size, 422 # use the value it writes to /sys/block/zram0/disksize. 423 if not os.path.exists(self.swap_disksize_file): 424 raise error.TestFail('The %s swap enable file should have' 425 ' caused zram to load, but %s was' 426 ' not found.' % 427 (enable_size, self.swap_disksize_file)) 428 disksize = utils.read_one_line(self.swap_disksize_file) 429 swaprequested = int(disksize) / 1000 430 if (swaptotal < swaprequested * 0.9 or 431 swaptotal > swaprequested * 1.1): 432 raise error.TestFail('Our swap of %d K is not within 10%%' 433 ' of the %d K we requested.' % 434 (swaptotal, swaprequested)) 435 logging.info('Swap enable (%s), requested %d, total %d', 436 enable_size, swaprequested, swaptotal) 437 438 # We should try to autodetect this if we add other swap methods. 439 swap_method = 'zram' 440 441 for swap_target in swap_targets: 442 logging.info('swap_target is %f', swap_target) 443 temp_dir = tempfile.mkdtemp() 444 try: 445 # Reset swap space to make sure nothing leaks between runs. 446 swap_reset = swap_reset_funcs[swap_method] 447 swap_reset() 448 self.run_single_test(compression_factor, num_procs, cycles, 449 swap_target, switch_delay, temp_dir, 450 selections) 451 except socket.error: 452 logging.debug('swap target %f failed; oom killer?', swap_target) 453 454 shutil.rmtree(temp_dir) 455 456