Home | History | Annotate | Download | only in platform_MemoryPressure
      1 # Copyright (c) 2012-2018 The Chromium OS Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 import logging, os, re, threading, time, utils
      6 
      7 from autotest_lib.client.bin import test
      8 from autotest_lib.client.common_lib import error
      9 from autotest_lib.client.common_lib.cros import chrome
     10 
     11 SYSFS_LOW_MEM_DIR = '/sys/kernel/mm/chromeos-low_mem/'
     12 SYSFS_MARGIN_FILENAME = SYSFS_LOW_MEM_DIR + 'margin'
     13 SYSFS_AVAILABLE_FILENAME = SYSFS_LOW_MEM_DIR + 'available'
     14 
     15 PF_MAX_RATE_PERIOD = 5.0  # seconds
     16 PF_RATE_INTERVAL = 10     # seconds
     17 
     18 def get_pf_count():
     19     """Returns the total number of major page faults since boot.
     20     """
     21     with open('/proc/vmstat') as f:
     22         return float(next((name_value for name_value in f.read().split('\n')
     23                            if 'pgmajfault ' in name_value)).split()[1])
     24 
     25 MEMINFO_RE = re.compile("^(\w+):\s+(\d+)")
     26 
     27 def get_meminfo():
     28     """Returns a dict of name-value pairs from /proc/meminfo.
     29     """
     30     info = {}
     31     with open('/proc/meminfo') as f:
     32         for line in f:
     33             m = MEMINFO_RE.match(line)
     34             if m:
     35                 info[m.group(1)] = int(m.group(2))
     36     return info
     37 
     38 class URLList:
     39     """A list of URLs as strings, plus a method for circular iteration."""
     40     def __init__(self):
     41         """Class initializer"""
     42         self.index = 0
     43 
     44     TAB_URLS = [
     45                 # commented-out URLs go to sign-in page
     46                 # 'https://mail.google.com',
     47                 # 'https://docs.google.com',
     48                 # 'https://calendar.google.com',
     49                 # 'https://keep.google.com',
     50                 # 'https://sites.google.com',
     51                 # 'https://play.google.com/movies',
     52                 'https://drive.google.com',
     53                 'https://photos.google.com',
     54                 'https://news.google.com',
     55                 'https://plus.google.com',
     56                 'https://maps.google.com',
     57                 'https://play.google.com/store',
     58                 'https://play.google.com/music',
     59                 'https://youtube.com',
     60                 'https://www.nytimes.com',
     61                 'https://www.whitehouse.gov',
     62                 'https://www.wsj.com',
     63                 'http://www.newsweek.com',            # seriously, http?
     64                 'https://www.washingtonpost.com',
     65                 'https://www.foxnews.com',
     66                 'https://www.nbc.com',
     67                 'https://www.amazon.com',
     68                 'https://www.walmart.com',
     69                 'https://www.target.com',
     70                 'https://www.facebook.com',
     71                 'https://www.cnn.com',
     72                 'https://www.cnn.com/us',
     73                 'https://www.cnn.com/world',
     74                 'https://www.cnn.com/politics',
     75                 'https://www.cnn.com/money',
     76                 'https://www.cnn.com/opinion',
     77                 'https://www.cnn.com/health',
     78                 'https://www.cnn.com/entertainment',
     79                 'https://www.cnn.com/tech',
     80                 'https://www.cnn.com/style',
     81                 'https://www.cnn.com/travel',
     82                 'https://www.cnn.com/sports',
     83                 'https://www.cnn.com/video',
     84                ]
     85 
     86     def next_url(self):
     87         """Returns the next URL from the list, looping over the list
     88            as needed.
     89         """
     90         url = self.TAB_URLS[self.index];
     91         self.index += 1
     92         if self.index == len(self.TAB_URLS):
     93             self.index = 0
     94         return url
     95 
     96 
     97 class PFMeter(threading.Thread):
     98     """A class whose instances measure page fault rates.  Each class instance
     99     (really only one is expected) runs a thread to keep track of the max rate.
    100     """
    101     def __init__(self):
    102         """Sets member variables."""
    103         threading.Thread.__init__(self)
    104         self.lock = threading.Lock()
    105         self.running = True
    106         self.reset()
    107         self.reset_max()
    108 
    109     def run(self):
    110         """Checks the average page fault rate across PF_MAX_RATE_PERIOD
    111            intervals, and records its maximum value.
    112         """
    113         logging.info("starting PFMeter thread")
    114         while self.running:
    115             time.sleep(PF_MAX_RATE_PERIOD)
    116             self.record_max()
    117         logging.info("PFMeter thread exiting")
    118 
    119     def stop(self):
    120         """Stops the thread."""
    121         self.running = False
    122 
    123     def reset(self):
    124         """Resets the meter for the purpose of computing the average rate.
    125            The max rate is reset with reset_max().
    126         """
    127         self.clock_start = time.time()
    128         self.count_start = get_pf_count()
    129 
    130     def reset_max(self):
    131         """Resets to 0 the max page fault rate seen thus far."""
    132         with self.lock:
    133             self.max_pf_rate = 0.0;
    134             self.max_clock_start = time.time()
    135             self.max_count_start = get_pf_count()
    136 
    137     def rate(self):
    138         """Returns the page fault rate (faults per second) since the last
    139         call to self (or to reset()).
    140         """
    141         clock_end = time.time()
    142         # Avoid division by zero (however unlikely) by waiting a little.
    143         if clock_end == self.clock_start:
    144             time.sleep(0.1)
    145             clock_end = time.time()
    146         delta_clock = clock_end - self.clock_start
    147         count_end = get_pf_count()
    148         delta_count = count_end - self.count_start
    149 
    150         rate = delta_count / delta_clock
    151 
    152         self.clock_start = clock_end
    153         self.count_start = count_end
    154 
    155         return rate
    156 
    157     def record_max(self):
    158         """Computes the average page fault rate since the last call to self, and
    159            saves the maximum values seen since the last call to reset_max().
    160         """
    161         clock = time.time()
    162         count = get_pf_count()
    163         with self.lock:
    164             long_interval_rate = ((count - self.max_count_start) /
    165                                   (clock - self.max_clock_start))
    166             self.max_count_start = count
    167             self.max_clock_start = clock
    168         if long_interval_rate > self.max_pf_rate:
    169             self.max_pf_rate = long_interval_rate
    170             logging.info('max pf rate = %.2f', self.max_pf_rate)
    171 
    172     def max_rate(self):
    173         """Returns the max pf rate seen since the last reset_max().
    174         """
    175         with self.lock:
    176             m = self.max_pf_rate
    177         return m
    178 
    179 def run_realistic_memory_pressure_test(time_limit, tab_open_delay):
    180     """Runs a memory pressure test using live pages.
    181     """
    182     perf_results = {}
    183     tab_switch_delay = 0.1
    184     pf_meter = PFMeter()
    185     url_list = URLList()
    186 
    187     pf_meter.start()
    188 
    189     with chrome.Chrome() as cr:
    190         tabs = []
    191         # Phase 1 (CREATE): open tabs and cycle through them until first
    192         # discard.
    193         start_time = time.time()
    194         # The first tab was created at browser start.
    195         created_tab_count = 1
    196         logging.info('START OF PHASE 1')
    197         while True:
    198             for _ in range(1):
    199                 tab = cr.browser.tabs.New()
    200                 tabs.append(tab)
    201                 created_tab_count += 1
    202                 # We number tabs starting at 1.
    203                 logging.info('navigating tab %d', created_tab_count)
    204                 tab.Navigate(url_list.next_url());
    205                 try:
    206                     url = tab.url
    207                     tab.action_runner.WaitForNetworkQuiescence(
    208                         timeout_in_seconds=30)
    209                 except Exception as e:
    210                     logging.warning('network wait exception %s at %s', e, url)
    211 
    212             # Activate tabs to increase the working set.
    213             for tab in tabs:
    214                 try:
    215                     tab.Activate()
    216                     time.sleep(tab_switch_delay)
    217                     # Check for tab discard.  |cr.browser.tabs| is actually a
    218                     # method call which returns the number of non-discarded
    219                     # tabs.
    220                     if created_tab_count > len(cr.browser.tabs):
    221                         break
    222                 except:
    223                     logging.info('tab activation failed, assuming tab discard')
    224                     break;
    225 
    226             tab_count = len(cr.browser.tabs)
    227             # Check for tab discard one more time.
    228             if created_tab_count > tab_count:
    229                 break
    230 
    231             if time.time() > time_limit:
    232                 logging.info('test timeout')
    233                 raise error.TestError('FAIL: phase 1 timeout with %d tabs' %
    234                                       created_tab_count)
    235 
    236         # Measure the page fault rate.
    237         pf_meter.reset()
    238         time.sleep(PF_RATE_INTERVAL)
    239         current_pf_rate = pf_meter.rate()
    240         max_pf_rate = pf_meter.max_rate()
    241         pf_meter.reset_max()
    242 
    243         discard_count = created_tab_count - tab_count
    244         elapsed_time = time.time() - start_time
    245         logging.info('opened %d tabs, %d discards',
    246                      created_tab_count, discard_count)
    247         logging.info('highest pf rate = %.2f pf/s', max_pf_rate)
    248         logging.info('elapsed time %.2f', elapsed_time)
    249         logging.info('END OF PHASE 1')
    250         meminfo = get_meminfo()
    251         perf_results['MemTotal'] = meminfo['MemTotal']
    252         perf_results['SwapTotal'] = meminfo['SwapTotal']
    253 
    254         perf_results['Phase1TabCount'] = tab_count
    255         perf_results['Phase1DiscardCount'] = discard_count
    256         perf_results['Phase1Time'] = time.time() - start_time
    257         perf_results['Phase1MaxPageFaultRate'] = max_pf_rate
    258         perf_results['Phase1PageFaultRate'] = current_pf_rate
    259         perf_results['Phase1MemFree'] = meminfo['MemFree']
    260         perf_results['Phase1SwapFree'] = meminfo['SwapFree']
    261 
    262         # Phase 2 (QUIESCE): Stop all activity.
    263         logging.info('START OF PHASE 2')
    264         start_time = time.time()
    265 
    266         # Measure the final pf rate, after one additional sleep.
    267         time.sleep(60)
    268         pf_meter.reset()
    269         time.sleep(PF_RATE_INTERVAL)
    270         current_pf_rate = pf_meter.rate()
    271         max_pf_rate = pf_meter.max_rate()
    272 
    273         pf_meter.stop()
    274 
    275         elapsed_time = time.time() - start_time
    276         browser_tab_count = len(cr.browser.tabs)
    277         discard_count = len(tabs) + 1 - browser_tab_count
    278         meminfo = get_meminfo()
    279         perf_results['Phase2Time'] = elapsed_time
    280         perf_results['Phase2TabCount'] = browser_tab_count
    281         perf_results['Phase2DiscardCount'] = discard_count
    282         perf_results['Phase2MaxPageFaultRate'] = max_pf_rate
    283         perf_results['Phase2PageFaultRate'] = current_pf_rate
    284         perf_results['Phase2MemFree'] = meminfo['MemFree']
    285         perf_results['Phase2SwapFree'] = meminfo['SwapFree']
    286         logging.info('after quiesce: discard count %d, pf rate = %.2f',
    287                       discard_count, current_pf_rate)
    288         logging.info('END OF PHASE 2')
    289 
    290 
    291         return perf_results
    292 
    293 def run_simple_tab_discard_test(time_limit, tab_open_delay_seconds, bindir):
    294     """
    295     Tests that tab discarding works correctly by using a small JS program
    296     which uses a lot of memory.
    297     """
    298     # 1 for initial tab opened
    299     n_tabs = 1
    300     discard = False
    301     perf_results = {}
    302     start_time = time.time()
    303     margin = int(utils.read_file(SYSFS_MARGIN_FILENAME))
    304 
    305     # Open tabs until a tab discard notification arrives, or a time limit
    306     # is reached.
    307     with chrome.Chrome(init_network_controller=True) as cr:
    308         cr.browser.platform.SetHTTPServerDirectories(bindir)
    309         while True:
    310             tab = cr.browser.tabs.New()
    311             n_tabs += 1
    312             # The program in js-bloat.html allocates a few large arrays and
    313             # forces them in memory by touching some of their elements.
    314             tab.Navigate(cr.browser.platform.http_server.UrlOf(
    315                     os.path.join(bindir, 'js-bloat.html')))
    316             tab.WaitForDocumentReadyStateToBeComplete()
    317             available = int(utils.read_file(SYSFS_LOW_MEM_DIR + 'available'))
    318             # Slow down when getting close to the discard margin, to avoid OOM
    319             # kills.
    320             time.sleep(tab_open_delay_seconds
    321                        if available > 3 * margin else
    322                        tab_open_delay_seconds * 3)
    323             if n_tabs > len(cr.browser.tabs):
    324                 logging.info('tab discard after %d tabs', n_tabs)
    325                 break
    326             if time.time() > time_limit:
    327                 raise error.TestError('FAIL: no tab discard after opening %d '
    328                                       'tabs in %ds' %
    329                                       (n_tabs, time_limit - start_time))
    330     perf_results["TabCountAtFirstDiscard"] = n_tabs
    331     return perf_results
    332 
    333 
    334 def run_quick_tab_discard_test(time_limit, bindir):
    335     """
    336     Tests that tab discarding works correctly allocating a few tabs,
    337     then increasing the discard margin until a discard occurs.
    338     """
    339     # 1 for initial tab opened
    340     n_tabs = 1
    341     discard = False
    342     perf_results = {}
    343     start_time = time.time()
    344     original_margin = int(utils.read_file(SYSFS_MARGIN_FILENAME))
    345 
    346     # Open 5 tabs, so the discarder has something to work with.
    347     # No need to be slow here since we'll be far from OOM.
    348     with chrome.Chrome(init_network_controller=True) as cr:
    349         cr.browser.platform.SetHTTPServerDirectories(bindir)
    350         for _ in range(5):
    351             tab = cr.browser.tabs.New()
    352             n_tabs += 1
    353             # The program in js-bloat.html allocates a few large arrays and
    354             # forces them in memory by touching some of their elements.
    355             tab.Navigate(cr.browser.platform.http_server.UrlOf(
    356                     os.path.join(bindir, 'js-bloat.html')))
    357             tab.WaitForDocumentReadyStateToBeComplete()
    358 
    359         if n_tabs > len(cr.browser.tabs):
    360             raise error.TestError('unexpected early discard')
    361 
    362         try:
    363             # Increase margin to 50MB above available. Discard should happen
    364             # immediately, but available may change, so keep trying.
    365             attempt_count = 0
    366             while True:
    367                 attempt_count += 1
    368                 available = int(utils.read_file(SYSFS_AVAILABLE_FILENAME))
    369                 logging.warning('TEMPORARILY CHANGING DISCARD MARGIN'
    370                                 ' --- do not interrupt the test')
    371                 utils.open_write_close(SYSFS_MARGIN_FILENAME,
    372                                        "%d" % (available + 50))
    373                 # Give chrome ample time to discard tabs.
    374                 time.sleep(2)
    375                 if n_tabs > len(cr.browser.tabs):
    376                     logging.info('tab discard after %d attempts', attempt_count)
    377                     break
    378                 if time.time() > time_limit:
    379                     raise error.TestError('FAIL: no tab discard in %d seconds' %
    380                                           time_limit - start_time)
    381         finally:
    382             utils.open_write_close(SYSFS_MARGIN_FILENAME, str(original_margin))
    383             logging.warning('discard margin was restored')
    384 
    385     perf_results["DiscardAttemptCount"] = attempt_count
    386     return perf_results
    387 
    388 
    389 class platform_MemoryPressure(test.test):
    390     """Memory pressure test."""
    391     version = 1
    392 
    393     def run_once(self,
    394                  flavor='simple',
    395                  tab_open_delay_seconds=1.0,
    396                  timeout_seconds=1800):
    397         """Runs the test once.
    398         """
    399         time_limit = time.time() + timeout_seconds
    400 
    401         if flavor == 'simple':
    402             pkv = run_simple_tab_discard_test(time_limit,
    403                                               tab_open_delay_seconds,
    404                                               self.bindir)
    405         elif flavor == 'realistic':
    406             pkv = run_realistic_memory_pressure_test(time_limit,
    407                                                      tab_open_delay_seconds)
    408         elif flavor == 'quick':
    409             pkv = run_quick_tab_discard_test(time_limit, self.bindir)
    410         else:
    411             raise error.TestError('unexpected "flavor" parameter: %s' % flavor)
    412 
    413         self.write_perf_keyval(pkv)
    414