Home | History | Annotate | Download | only in video_VideoDecodeMemoryUsage
      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