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