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