1#!/usr/bin/python3 2# Copyright 2018 The Chromium OS Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6import logging 7import subprocess 8import time 9import threading 10 11import common 12 13from autotest_lib.client.bin import utils 14 15class MemoryEater(object): 16 """A util class which run programs to consume memory in the background. 17 18 Sample usage: 19 with MemoryEator() as memory_eater: 20 # Allocate mlocked memory. 21 memory_eater.consume_locked_memory(123) 22 23 # Allocate memory and sequentially traverse them over and over. 24 memory_eater.consume_active_memory(500) 25 26 When it goes out of the "with" context or the object is destructed, all 27 allocated memory are released. 28 """ 29 30 memory_eater_locked = 'memory-eater-locked' 31 memory_eater = 'memory-eater' 32 33 _all_instances = [] 34 35 def __init__(self): 36 self._locked_consumers = [] 37 self._active_consumers_lock = threading.Lock() 38 self._active_consumers = [] 39 self._all_instances.append(self) 40 41 def __enter__(self): 42 return self 43 44 @staticmethod 45 def cleanup_consumers(consumers): 46 """Kill all processes in |consumers| 47 48 @param consumers: The list of consumers to clean. 49 """ 50 while len(consumers): 51 job = consumers.pop() 52 logging.info('Killing %d', job.pid) 53 job.kill() 54 55 def cleanup(self): 56 """Releases all allocated memory.""" 57 # Kill all hanging jobs. 58 logging.info('Cleaning hanging memory consuming processes...') 59 self.cleanup_consumers(self._locked_consumers) 60 with self._active_consumers_lock: 61 self.cleanup_consumers(self._active_consumers) 62 63 def __exit__(self, type, value, traceback): 64 self.cleanup() 65 66 def __del__(self): 67 self.cleanup() 68 if self in self._all_instances: 69 self._all_instances.remove(self) 70 71 def consume_locked_memory(self, mb): 72 """Consume non-swappable memory.""" 73 logging.info('Consuming locked memory %d MB', mb) 74 cmd = [self.memory_eater_locked, str(mb)] 75 p = subprocess.Popen(cmd, stdout=subprocess.PIPE) 76 self._locked_consumers.append(p) 77 # Wait until memory allocation is done. 78 while True: 79 line = p.stdout.readline() 80 if line.find('Done') != -1: 81 break 82 83 def consume_active_memory(self, mb): 84 """Consume active memory.""" 85 logging.info('Consuming active memory %d MB', mb) 86 cmd = [self.memory_eater, '--size', str(mb), '--chunk', '128'] 87 p = subprocess.Popen(cmd, stdout=subprocess.PIPE) 88 with self._active_consumers_lock: 89 self._active_consumers.append(p) 90 91 @classmethod 92 def get_active_consumer_pids(cls): 93 """Gets pid of active consumers by all instances of the class.""" 94 all_pids = [] 95 for instance in cls._all_instances: 96 with instance._active_consumers_lock: 97 all_pids.extend([p.pid for p in instance._active_consumers]) 98 return all_pids 99 100 101def consume_free_memory(memory_to_reserve_mb): 102 """Consumes free memory until |memory_to_reserve_mb| is remained. 103 104 Non-swappable memory is allocated to consume memory. 105 memory_to_reserve_mb: Consume memory until this amount of free memory 106 is remained. 107 @return The MemoryEater() object on which memory is allocated. One can 108 catch it in a context manager. 109 """ 110 consumer = MemoryEater() 111 while True: 112 mem_free_mb = utils.read_from_meminfo('MemFree') / 1024 113 logging.info('Current Free Memory %d', mem_free_mb) 114 if mem_free_mb <= memory_to_reserve_mb: 115 break 116 memory_to_consume = min( 117 2047, mem_free_mb - memory_to_reserve_mb + 1) 118 logging.info('Consuming %d MB locked memory', memory_to_consume) 119 consumer.consume_locked_memory(memory_to_consume) 120 return consumer 121 122 123class TimeoutException(Exception): 124 """Exception to return if timeout happens.""" 125 def __init__(self, message): 126 super(TimeoutException, self).__init__(message) 127 128 129class _Timer(object): 130 """A simple timer class to check timeout.""" 131 def __init__(self, timeout, des): 132 """Initializer. 133 134 @param timeout: Timeout in seconds. 135 @param des: A short description for this timer. 136 """ 137 self.timeout = timeout 138 self.des = des 139 if self.timeout: 140 self.start_time = time.time() 141 142 def check_timeout(self): 143 """Raise TimeoutException if timeout happens.""" 144 if not self.timeout: 145 return 146 time_delta = time.time() - self.start_time 147 if time_delta > self.timeout: 148 err_message = '%s timeout after %s seconds' % (self.des, time_delta) 149 logging.warning(err_message) 150 raise TimeoutException(err_message) 151 152 153def run_single_memory_pressure( 154 starting_mb, step_mb, end_condition, duration, cool_down, timeout=None): 155 """Runs a single memory consumer to produce memory pressure. 156 157 Keep adding memory pressure. In each round, it runs a memory consumer 158 and waits for a while before checking whether to end the process. If not, 159 kill current memory consumer and allocate more memory pressure in the next 160 round. 161 @param starting_mb: The amount of memory to start with. 162 @param step_mb: If |end_condition| is not met, allocate |step_mb| more 163 memory in the next round. 164 @param end_condition: A boolean function returns whether to end the process. 165 @param duration: Time (in seconds) to wait between running a memory 166 consumer and checking |end_condition|. 167 @param cool_down: Time (in seconds) to wait between each round. 168 @param timeout: Seconds to stop the function is |end_condition| is not met. 169 @return The size of memory allocated in the last round. 170 @raise TimeoutException if timeout. 171 """ 172 current_mb = starting_mb 173 timer = _Timer(timeout, 'run_single_memory_pressure') 174 while True: 175 timer.check_timeout() 176 with MemoryEater() as consumer: 177 consumer.consume_active_memory(current_mb) 178 time.sleep(duration) 179 if end_condition(): 180 return current_mb 181 current_mb += step_mb 182 time.sleep(cool_down) 183 184 185def run_multi_memory_pressure(size_mb, end_condition, duration, timeout=None): 186 """Runs concurrent memory consumers to produce memory pressure. 187 188 In each round, it runs a new memory consumer until a certain condition is 189 met. 190 @param size_mb: The amount of memory each memory consumer allocates. 191 @param end_condition: A boolean function returns whether to end the process. 192 @param duration: Time (in seconds) to wait between running a memory 193 consumer and checking |end_condition|. 194 @param timeout: Seconds to stop the function is |end_condition| is not met. 195 @return Total allocated memory. 196 @raise TimeoutException if timeout. 197 """ 198 total_mb = 0 199 timer = _Timer(timeout, 'run_multi_memory_pressure') 200 with MemoryEater() as consumer: 201 while True: 202 timer.check_timeout() 203 consumer.consume_active_memory(size_mb) 204 time.sleep(duration) 205 if end_condition(): 206 return total_mb 207 total_mb += size_mb 208