1# Copyright (c) 2013 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 5import logging 6import os 7import re 8import time 9 10from math import sqrt 11 12from autotest_lib.client.bin import test, utils 13from autotest_lib.client.common_lib import error 14from autotest_lib.client.common_lib.cros import chrome 15from autotest_lib.client.cros.graphics import graphics_utils 16from autotest_lib.client.cros.video import helper_logger 17 18TEST_PAGE = 'content.html' 19 20# The keys to access the content of memry stats. 21KEY_RENDERER = 'Renderer' 22KEY_BROWSER = 'Browser' 23KEY_GPU = 'Gpu' 24KEY_RSS = 'WorkingSetSize' 25 26# The number of iterations to be run before measuring the memory usage. 27# Just ensure we have fill up the caches/buffers so that we can get 28# a more stable/correct result. 29WARMUP_COUNT = 50 30 31# Number of iterations per measurement. 32EVALUATION_COUNT = 70 33 34# The minimal number of samples for memory-leak test. 35MEMORY_LEAK_CHECK_MIN_COUNT = 20 36 37# The approximate values of the student's t-distribution at 95% confidence. 38# See http://en.wikipedia.org/wiki/Student's_t-distribution 39T_095 = [None, # No value for degree of freedom 0 40 12.706205, 4.302653, 3.182446, 2.776445, 2.570582, 2.446912, 2.364624, 41 2.306004, 2.262157, 2.228139, 2.200985, 2.178813, 2.160369, 2.144787, 42 2.131450, 2.119905, 2.109816, 2.100922, 2.093024, 2.085963, 2.079614, 43 2.073873, 2.068658, 2.063899, 2.059539, 2.055529, 2.051831, 2.048407, 44 2.045230, 2.042272, 2.039513, 2.036933, 2.034515, 2.032245, 2.030108, 45 2.028094, 2.026192, 2.024394, 2.022691, 2.021075, 2.019541, 2.018082, 46 2.016692, 2.015368, 2.014103, 2.012896, 2.011741, 2.010635, 2.009575, 47 2.008559, 2.007584, 2.006647, 2.005746, 2.004879, 2.004045, 2.003241, 48 2.002465, 2.001717, 2.000995, 2.000298, 1.999624, 1.998972, 1.998341, 49 1.997730, 1.997138, 1.996564, 1.996008, 1.995469, 1.994945, 1.994437, 50 1.993943, 1.993464, 1.992997, 1.992543, 1.992102, 1.991673, 1.991254, 51 1.990847, 1.990450, 1.990063, 1.989686, 1.989319, 1.988960, 1.988610, 52 1.988268, 1.987934, 1.987608, 1.987290, 1.986979, 1.986675, 1.986377, 53 1.986086, 1.985802, 1.985523, 1.985251, 1.984984, 1.984723, 1.984467, 54 1.984217, 1.983972, 1.983731] 55 56# The memory leak (bytes/iteration) we can tolerate. 57MEMORY_LEAK_THRESHOLD = 1024 * 1024 58 59# Regular expression used to parse the content of '/proc/meminfo' 60# The content of the file looks like: 61# MemTotal: 65904768 kB 62# MemFree: 14248152 kB 63# Buffers: 508836 kB 64MEMINFO_RE = re.compile('^(\w+):\s+(\d+)', re.MULTILINE) 65MEMINFO_PATH = '/proc/meminfo' 66 67# We sum up the following values in '/proc/meminfo' to represent 68# the kernel memory usage. 69KERNEL_MEMORY_ENTRIES = ['Slab', 'Shmem', 'KernelStack', 'PageTables'] 70 71MEM_TOTAL_ENTRY = 'MemTotal' 72 73# The default sleep time, in seconds. 74SLEEP_TIME = 1.5 75 76 77def _get_kernel_memory_usage(): 78 with file(MEMINFO_PATH) as f: 79 mem_info = {x.group(1): int(x.group(2)) 80 for x in MEMINFO_RE.finditer(f.read())} 81 # Sum up the kernel memory usage (in KB) in mem_info 82 return sum(map(mem_info.get, KERNEL_MEMORY_ENTRIES)) 83 84def _get_graphics_memory_usage(): 85 """Get the memory usage (in KB) of the graphics module.""" 86 key = 'gem_objects_bytes' 87 graphics_kernel_memory = graphics_utils.GraphicsKernelMemory() 88 usage = graphics_kernel_memory.get_memory_keyvals().get(key, 0) 89 90 if graphics_kernel_memory.num_errors: 91 logging.warning('graphics memory info is not available') 92 return 0 93 94 return usage 95 96def _get_linear_regression_slope(x, y): 97 """ 98 Gets slope and the confidence interval of the linear regression based on 99 the given xs and ys. 100 101 This function returns a tuple (beta, delta), where the beta is the slope 102 of the linear regression and delta is the range of the confidence 103 interval, i.e., confidence interval = (beta + delta, beta - delta). 104 """ 105 assert len(x) == len(y) 106 n = len(x) 107 sx, sy = sum(x), sum(y) 108 sxx = sum(v * v for v in x) 109 syy = sum(v * v for v in y) 110 sxy = sum(u * v for u, v in zip(x, y)) 111 beta = float(n * sxy - sx * sy) / (n * sxx - sx * sx) 112 alpha = float(sy - beta * sx) / n 113 stderr2 = (n * syy - sy * sy - 114 beta * beta * (n * sxx - sx * sx)) / (n * (n - 2)) 115 std_beta = sqrt((n * stderr2) / (n * sxx - sx * sx)) 116 return (beta, T_095[n - 2] * std_beta) 117 118 119def _assert_no_memory_leak(name, mem_usage, threshold = MEMORY_LEAK_THRESHOLD): 120 """Helper function to check memory leak""" 121 index = range(len(mem_usage)) 122 slope, delta = _get_linear_regression_slope(index, mem_usage) 123 logging.info('confidence interval: %s - %s, %s', 124 name, slope - delta, slope + delta) 125 if (slope - delta > threshold): 126 logging.debug('memory usage for %s - %s', name, mem_usage) 127 raise error.TestError('leak detected: %s - %s' % (name, slope - delta)) 128 129 130def _output_entries(out, entries): 131 out.write(' '.join(str(x) for x in entries) + '\n') 132 out.flush() 133 134 135class MemoryTest(object): 136 """The base class of all memory tests""" 137 138 def __init__(self, bindir): 139 self._bindir = bindir 140 141 142 def _open_new_tab(self, page_to_open): 143 tab = self.browser.tabs.New() 144 tab.Activate() 145 tab.Navigate(self.browser.platform.http_server.UrlOf( 146 os.path.join(self._bindir, page_to_open))) 147 tab.WaitForDocumentReadyStateToBeComplete() 148 return tab 149 150 151 def _get_memory_usage(self): 152 """Helper function to get the memory usage. 153 154 It returns a tuple of six elements: 155 (browser_usage, renderer_usage, gpu_usage, kernel_usage, 156 total_usage, graphics_usage) 157 All are expected in the unit of KB. 158 159 browser_usage: the RSS of the browser process 160 rednerers_usage: the total RSS of all renderer processes 161 rednerers_usage: the total RSS of all gpu processes 162 kernel_usage: the memory used in kernel 163 total_usage: the sum of the above memory usages. The graphics_usage is 164 not included because the composition of the graphics 165 memory is much more complicated (could be from video card, 166 user space, or kenerl space). It doesn't make so much 167 sense to sum it up with others. 168 graphics_usage: the memory usage reported by the graphics driver 169 """ 170 # Force to collect garbage before measuring memory 171 for i in xrange(len(self.browser.tabs)): 172 # TODO(owenlin): Change to "for t in tabs" once 173 # http://crbug.com/239735 is resolved 174 self.browser.tabs[i].CollectGarbage() 175 176 m = self.browser.memory_stats 177 178 result = (m[KEY_BROWSER][KEY_RSS] / 1024, 179 m[KEY_RENDERER][KEY_RSS] / 1024, 180 m[KEY_GPU][KEY_RSS] / 1024, 181 _get_kernel_memory_usage()) 182 183 # total = browser + renderer + gpu + kernal 184 result += (sum(result), _get_graphics_memory_usage()) 185 return result 186 187 188 def initialize(self): 189 """A callback function. It is just called before the main loops.""" 190 pass 191 192 193 def loop(self): 194 """A callback function. It is the main memory test function.""" 195 pass 196 197 198 def cleanup(self): 199 """A callback function, executed after loop().""" 200 pass 201 202 203 def run(self, name, browser, videos, test, 204 warmup_count=WARMUP_COUNT, 205 eval_count=EVALUATION_COUNT): 206 """Runs this memory test case. 207 208 @param name: the name of the test. 209 @param browser: the telemetry entry of the browser under test. 210 @param videos: the videos to be used in the test. 211 @param test: the autotest itself, used to output performance values. 212 @param warmup_count: run loop() for warmup_count times to make sure the 213 memory usage has been stabalize. 214 @param eval_count: run loop() for eval_count times to measure the memory 215 usage. 216 """ 217 218 self.browser = browser 219 self.videos = videos 220 self.name = name 221 222 names = ['browser', 'renderers', 'gpu', 'kernel', 'total', 'graphics'] 223 result_log = open(os.path.join(test.resultsdir, '%s.log' % name), 'wt') 224 _output_entries(result_log, names) 225 226 self.initialize() 227 try: 228 for i in xrange(warmup_count): 229 self.loop() 230 _output_entries(result_log, self._get_memory_usage()) 231 232 metrics = [] 233 for i in xrange(eval_count): 234 self.loop() 235 results = self._get_memory_usage() 236 _output_entries(result_log, results) 237 metrics.append(results) 238 239 # Check memory leak when we have enough samples 240 if len(metrics) >= MEMORY_LEAK_CHECK_MIN_COUNT: 241 # Assert no leak in the 'total' and 'graphics' usages 242 for index in map(names.index, ('total', 'graphics')): 243 _assert_no_memory_leak( 244 self.name, [m[index] for m in metrics]) 245 246 indices = range(len(metrics)) 247 248 # Prefix the test name to each metric's name 249 fullnames = ['%s.%s' % (name, n) for n in names] 250 251 # Transpose metrics, and iterate each type of memory usage 252 for name, metric in zip(fullnames, zip(*metrics)): 253 memory_increase_per_run, _ = _get_linear_regression_slope( 254 indices, metric) 255 logging.info('memory increment for %s - %s', 256 name, memory_increase_per_run) 257 test.output_perf_value(description=name, 258 value=memory_increase_per_run, 259 units='KB', higher_is_better=False) 260 finally: 261 self.cleanup() 262 263 264def _change_source_and_play(tab, video): 265 tab.EvaluateJavaScript('changeSourceAndPlay("%s")' % video) 266 267 268def _assert_video_is_playing(tab): 269 if not tab.EvaluateJavaScript('isVideoPlaying()'): 270 raise error.TestError('video is stopped') 271 272 # The above check may fail. Be sure the video time is advancing. 273 startTime = tab.EvaluateJavaScript('getVideoCurrentTime()') 274 275 def _is_video_playing(): 276 return startTime != tab.EvaluateJavaScript('getVideoCurrentTime()') 277 278 utils.poll_for_condition( 279 _is_video_playing, exception=error.TestError('video is stuck')) 280 281 282class OpenTabPlayVideo(MemoryTest): 283 """A memory test case: 284 Open a tab, play a video and close the tab. 285 """ 286 287 def loop(self): 288 tab = self._open_new_tab(TEST_PAGE) 289 _change_source_and_play(tab, self.videos[0]) 290 _assert_video_is_playing(tab) 291 time.sleep(SLEEP_TIME) 292 tab.Close() 293 294 # Wait a while for the closed tab to clean up all used resources 295 time.sleep(SLEEP_TIME) 296 297 298class PlayVideo(MemoryTest): 299 """A memory test case: keep playing a video.""" 300 301 def initialize(self): 302 super(PlayVideo, self).initialize() 303 self.activeTab = self._open_new_tab(TEST_PAGE) 304 _change_source_and_play(self.activeTab, self.videos[0]) 305 306 307 def loop(self): 308 time.sleep(SLEEP_TIME) 309 _assert_video_is_playing(self.activeTab) 310 311 312 def cleanup(self): 313 self.activeTab.Close() 314 315 316class ChangeVideoSource(MemoryTest): 317 """A memory test case: change the "src" property of <video> object to 318 load different video sources.""" 319 320 def initialize(self): 321 super(ChangeVideoSource, self).initialize() 322 self.activeTab = self._open_new_tab(TEST_PAGE) 323 324 325 def loop(self): 326 for video in self.videos: 327 _change_source_and_play(self.activeTab, video) 328 time.sleep(SLEEP_TIME) 329 _assert_video_is_playing(self.activeTab) 330 331 332 def cleanup(self): 333 self.activeTab.Close() 334 335 336def _get_testcase_name(class_name, videos): 337 # Convert from Camel to underscrore. 338 s = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', class_name) 339 s = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s).lower() 340 341 # Get a shorter name from the first video's URL. 342 # For example, get 'tp101.mp4' from the URL: 343 # 'http://host/path/tpe101-1024x768-9123456780123456.mp4' 344 m = re.match('.*/(\w+)-.*\.(\w+)', videos[0]) 345 346 return '%s.%s.%s' % (m.group(1), m.group(2), s) 347 348 349# Deprecate the logging messages at DEBUG level (and lower) in telemetry. 350# http://crbug.com/331992 351class TelemetryFilter(logging.Filter): 352 """Filter for telemetry logging.""" 353 354 def filter(self, record): 355 return (record.levelno > logging.DEBUG or 356 'telemetry' not in record.pathname) 357 358 359class video_VideoDecodeMemoryUsage(test.test): 360 """This is a memory usage test for video playback.""" 361 version = 1 362 363 @helper_logger.video_log_wrapper 364 def run_once(self, testcases): 365 last_error = None 366 logging.getLogger().addFilter(TelemetryFilter()) 367 368 with chrome.Chrome( 369 extra_browser_args=helper_logger.chrome_vmodule_flag(), 370 init_network_controller=True) as cr: 371 cr.browser.platform.SetHTTPServerDirectories(self.bindir) 372 for class_name, videos in testcases: 373 name = _get_testcase_name(class_name, videos) 374 logging.info('run: %s - %s', name, videos) 375 try : 376 test_case_class = globals()[class_name] 377 test_case_class(self.bindir).run( 378 name, cr.browser, videos, self) 379 except Exception as last_error: 380 logging.exception('%s fail', name) 381 # continue to next test case 382 383 if last_error: 384 raise # the last_error 385