xref: /aosp_15_r20/external/autotest/client/common_lib/cros/memory_eater.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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