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