Home | History | Annotate | Download | only in platform_CompressedSwapPerf
      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