• 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
15
16TEST_PAGE = 'content.html'
17
18# The keys to access the content of memry stats.
19KEY_RENDERER = 'Renderer'
20KEY_BROWSER = 'Browser'
21KEY_GPU = 'Gpu'
22KEY_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.
27WARMUP_COUNT = 50
28
29# Number of iterations per measurement.
30EVALUATION_COUNT = 70
31
32# The minimal number of samples for memory-leak test.
33MEMORY_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
37T_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.
55MEMORY_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
62MEMINFO_RE = re.compile('^(\w+):\s+(\d+)', re.MULTILINE)
63MEMINFO_PATH = '/proc/meminfo'
64
65# We sum up the following values in '/proc/meminfo' to represent
66# the kernel memory usage.
67KERNEL_MEMORY_ENTRIES = ['Slab', 'Shmem', 'KernelStack', 'PageTables']
68
69MEM_TOTAL_ENTRY = 'MemTotal'
70
71# Paths of files to read graphics memory usage from
72X86_GEM_OBJECTS_PATH = '/sys/kernel/debug/dri/0/i915_gem_objects'
73ARM_GEM_OBJECTS_PATH = '/sys/kernel/debug/dri/0/exynos_gem_objects'
74
75GEM_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"
81GEM_OBJECTS_RE = re.compile('(\d+)\s+objects,\s+(\d+)\s+bytes')
82
83# The default sleep time, in seconds.
84SLEEP_TIME = 1.5
85
86
87def _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
95def _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
111def _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
134def _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
145def _output_entries(out, entries):
146    out.write(' '.join(str(x) for x in entries) + '\n')
147    out.flush()
148
149
150class 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
281def _change_source_and_play(tab, video):
282    tab.EvaluateJavaScript('changeSourceAndPlay("%s")' % video)
283
284
285def _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
299class 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
315class 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
333class 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
353def _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
368class TelemetryFilter(logging.Filter):
369
370    def filter(self, record):
371        return (record.levelno > logging.DEBUG or
372            'telemetry' not in record.pathname)
373
374
375class 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