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 cStringIO, collections, dbus, gzip, logging, subprocess
6
7from autotest_lib.client.bin import test
8from autotest_lib.client.common_lib import error
9
10
11class platform_DebugDaemonGetPerfData(test.test):
12    """
13    This autotest tests the collection of perf data.  It calls perf indirectly
14    through debugd -> quipper -> perf.
15
16    The perf data is collected both when the system is idle and when there is a
17    process running in the background.
18
19    The perf data is collected over various durations.
20    """
21
22    version = 1
23
24    # A list of durations over which to gather perf data using quipper (given in
25    # seconds), plus the number of times to run perf with each duration.
26    # e.g. the entry "1: 50" means to run perf for 1 second 50 times.
27    _profile_duration_and_repetitions = [
28        (1, 3),
29        (5, 1)
30    ]
31
32    # Commands to repeatedly run in the background when collecting perf data.
33    _system_load_commands = {
34        'idle'     : 'sleep 1',
35        'busy'     : 'ls',
36    }
37
38    _dbus_debugd_object = '/org/chromium/debugd'
39    _dbus_debugd_name = 'org.chromium.debugd'
40
41    # For storing the size of returned results.
42    SizeInfo = collections.namedtuple('SizeInfo', ['size', 'size_zipped'])
43
44    def gzip_string(self, string):
45        """
46        Gzip a string.
47
48        @param string: The input string to be gzipped.
49
50        Returns:
51          The gzipped string.
52        """
53        string_file = cStringIO.StringIO()
54        gzip_file = gzip.GzipFile(fileobj=string_file, mode='wb')
55        gzip_file.write(string)
56        gzip_file.close()
57        return string_file.getvalue()
58
59
60    def validate_get_perf_method(self, duration, num_reps, load_type):
61        """
62        Validate a debugd method that returns perf data.
63
64        @param duration: The duration to use for perf data collection.
65        @param num_reps: Number of times to run.
66        @param load_type: A label to use for storing into perf keyvals.
67        """
68        # Dictionary for storing results returned from debugd.
69        # Key:   Name of data type (string)
70        # Value: Sizes of results in bytes (list of SizeInfos)
71        stored_results = collections.defaultdict(list)
72
73        for _ in range(num_reps):
74            perf_command = ['perf', 'record', '-a', '-e', 'cycles',
75                            '-c', '1000003']
76            status, perf_data, perf_stat = (
77                self.dbus_iface.GetPerfOutput(duration, perf_command))
78            if status != 0:
79                raise error.TestFail('GetPerfOutput() returned status %d',
80                                     status)
81            if len(perf_data) == 0 and len(perf_stat) == 0:
82                raise error.TestFail('GetPerfOutput() returned no data')
83            if len(perf_data) > 0 and len(perf_stat) > 0:
84                raise error.TestFail('GetPerfOutput() returned both '
85                                     'perf_data and perf_stat')
86
87            result_type = None
88            if perf_data:
89                result = perf_data
90                result_type = "perf_data"
91            else:   # if perf_stat
92                result = perf_stat
93                result_type = "perf_stat"
94
95            logging.info('GetPerfOutput() for %s seconds returned %d '
96                         'bytes of type %s',
97                         duration, len(result), result_type)
98            if len(result) < 10:
99                raise error.TestFail('Perf output too small')
100
101            # Convert |result| from an array of dbus.Bytes to a string.
102            result = ''.join(chr(b) for b in result)
103
104            # If there was an error in collecting a profile with quipper, debugd
105            # will output an error message. Make sure to check for this message.
106            # It is found in PerfTool::GetPerfDataHelper() in
107            # debugd/src/perf_tool.cc.
108            if result.startswith('<process exited with status: '):
109                raise error.TestFail('Quipper failed: %s' % result)
110
111            stored_results[result_type].append(
112                self.SizeInfo(len(result), len(self.gzip_string(result))))
113
114        for result_type, sizes in stored_results.iteritems():
115            key = 'mean_%s_size_%s_%d' % (result_type, load_type, duration)
116            total_size = sum(entry.size for entry in sizes)
117            total_size_zipped = sum(entry.size_zipped for entry in sizes)
118
119            keyvals = {}
120            keyvals[key] = total_size / len(sizes)
121            keyvals[key + '_zipped'] = total_size_zipped / len(sizes)
122            self.write_perf_keyval(keyvals)
123
124
125    def run_once(self, *args, **kwargs):
126        """
127        Primary autotest function.
128        """
129
130        bus = dbus.SystemBus()
131        proxy = bus.get_object(self._dbus_debugd_name, self._dbus_debugd_object)
132        self.dbus_iface = dbus.Interface(proxy,
133                                         dbus_interface=self._dbus_debugd_name)
134
135        # Open /dev/null to redirect unnecessary output.
136        devnull = open('/dev/null', 'w')
137
138        load_items = self._system_load_commands.iteritems()
139        for load_type, load_command in load_items:
140            # Repeatedly run the comand for the current load.
141            cmd = 'while true; do %s; done' % load_command
142            process = subprocess.Popen(cmd, stdout=devnull, shell=True)
143
144            for duration, num_reps in self._profile_duration_and_repetitions:
145                # Collect perf data from debugd.
146                self.validate_get_perf_method(duration, num_reps, load_type)
147
148            # Terminate the process and actually wait for it to terminate.
149            process.terminate()
150            while process.poll() == None:
151                pass
152
153        devnull.close()
154