• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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