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