xref: /aosp_15_r20/external/autotest/server/cros/resource_monitor.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# Lint as: python2, python3
2*9c5db199SXin Li# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be
4*9c5db199SXin Li# found in the LICENSE file.
5*9c5db199SXin Li
6*9c5db199SXin Lifrom __future__ import absolute_import
7*9c5db199SXin Lifrom __future__ import division
8*9c5db199SXin Lifrom __future__ import print_function
9*9c5db199SXin Li
10*9c5db199SXin Liimport logging
11*9c5db199SXin Liimport csv
12*9c5db199SXin Liimport six
13*9c5db199SXin Liimport random
14*9c5db199SXin Liimport re
15*9c5db199SXin Liimport collections
16*9c5db199SXin Li
17*9c5db199SXin Lifrom autotest_lib.client.common_lib.cros import path_utils
18*9c5db199SXin Li
19*9c5db199SXin Liclass ResourceMonitorRawResult(object):
20*9c5db199SXin Li    """Encapsulates raw resource_monitor results."""
21*9c5db199SXin Li
22*9c5db199SXin Li    def __init__(self, raw_results_filename):
23*9c5db199SXin Li        self._raw_results_filename = raw_results_filename
24*9c5db199SXin Li
25*9c5db199SXin Li
26*9c5db199SXin Li    def get_parsed_results(self):
27*9c5db199SXin Li        """Constructs parsed results from the raw ones.
28*9c5db199SXin Li
29*9c5db199SXin Li        @return ResourceMonitorParsedResult object
30*9c5db199SXin Li
31*9c5db199SXin Li        """
32*9c5db199SXin Li        return ResourceMonitorParsedResult(self.raw_results_filename)
33*9c5db199SXin Li
34*9c5db199SXin Li
35*9c5db199SXin Li    @property
36*9c5db199SXin Li    def raw_results_filename(self):
37*9c5db199SXin Li        """@return string filename storing the raw top command output."""
38*9c5db199SXin Li        return self._raw_results_filename
39*9c5db199SXin Li
40*9c5db199SXin Li
41*9c5db199SXin Liclass IncorrectTopFormat(Exception):
42*9c5db199SXin Li    """Thrown if top output format is not as expected"""
43*9c5db199SXin Li    pass
44*9c5db199SXin Li
45*9c5db199SXin Li
46*9c5db199SXin Lidef _extract_value_before_single_keyword(line, keyword):
47*9c5db199SXin Li    """Extract word occurring immediately before the specified keyword.
48*9c5db199SXin Li
49*9c5db199SXin Li    @param line string the line in which to search for the keyword.
50*9c5db199SXin Li    @param keyword string the keyword to look for. Can be a regexp.
51*9c5db199SXin Li    @return string the word just before the keyword.
52*9c5db199SXin Li
53*9c5db199SXin Li    """
54*9c5db199SXin Li    pattern = ".*?(\S+) " + keyword
55*9c5db199SXin Li    matches = re.match(pattern, line)
56*9c5db199SXin Li    if matches is None or len(matches.groups()) != 1:
57*9c5db199SXin Li        raise IncorrectTopFormat
58*9c5db199SXin Li
59*9c5db199SXin Li    return matches.group(1)
60*9c5db199SXin Li
61*9c5db199SXin Li
62*9c5db199SXin Lidef _extract_values_before_keywords(line, *args):
63*9c5db199SXin Li    """Extract the words occuring immediately before each specified
64*9c5db199SXin Li        keyword in args.
65*9c5db199SXin Li
66*9c5db199SXin Li    @param line string the string to look for the keywords.
67*9c5db199SXin Li    @param args variable number of string args the keywords to look for.
68*9c5db199SXin Li    @return string list the words occuring just before each keyword.
69*9c5db199SXin Li
70*9c5db199SXin Li    """
71*9c5db199SXin Li    line_nocomma = re.sub(",", " ", line)
72*9c5db199SXin Li    line_singlespace = re.sub("\s+", " ", line_nocomma)
73*9c5db199SXin Li
74*9c5db199SXin Li    return [_extract_value_before_single_keyword(
75*9c5db199SXin Li            line_singlespace, arg) for arg in args]
76*9c5db199SXin Li
77*9c5db199SXin Li
78*9c5db199SXin Lidef _find_top_output_identifying_pattern(line):
79*9c5db199SXin Li    """Return true iff the line looks like the first line of top output.
80*9c5db199SXin Li
81*9c5db199SXin Li    @param line string to look for the pattern
82*9c5db199SXin Li    @return boolean
83*9c5db199SXin Li
84*9c5db199SXin Li    """
85*9c5db199SXin Li    pattern ="\s*top\s*-.*up.*users.*"
86*9c5db199SXin Li    matches = re.match(pattern, line)
87*9c5db199SXin Li    return matches is not None
88*9c5db199SXin Li
89*9c5db199SXin Li
90*9c5db199SXin Liclass ResourceMonitorParsedResult(object):
91*9c5db199SXin Li    """Encapsulates logic to parse and represent top command results."""
92*9c5db199SXin Li
93*9c5db199SXin Li    _columns = ["Time", "UserCPU", "SysCPU", "NCPU", "Idle",
94*9c5db199SXin Li            "IOWait", "IRQ", "SoftIRQ", "Steal",
95*9c5db199SXin Li            "MemUnits", "UsedMem", "FreeMem",
96*9c5db199SXin Li            "SwapUnits", "UsedSwap", "FreeSwap"]
97*9c5db199SXin Li    UtilValues = collections.namedtuple('UtilValues', ' '.join(_columns))
98*9c5db199SXin Li
99*9c5db199SXin Li    def __init__(self, raw_results_filename):
100*9c5db199SXin Li        """Construct a ResourceMonitorResult.
101*9c5db199SXin Li
102*9c5db199SXin Li        @param raw_results_filename string filename of raw batch top output.
103*9c5db199SXin Li
104*9c5db199SXin Li        """
105*9c5db199SXin Li        self._raw_results_filename = raw_results_filename
106*9c5db199SXin Li        self.parse_resource_monitor_results()
107*9c5db199SXin Li
108*9c5db199SXin Li
109*9c5db199SXin Li    def parse_resource_monitor_results(self):
110*9c5db199SXin Li        """Extract utilization metrics from output file."""
111*9c5db199SXin Li        self._utils_over_time = []
112*9c5db199SXin Li
113*9c5db199SXin Li        with open(self._raw_results_filename, "r") as results_file:
114*9c5db199SXin Li            while True:
115*9c5db199SXin Li                curr_line = '\n'
116*9c5db199SXin Li                while curr_line != '' and \
117*9c5db199SXin Li                        not _find_top_output_identifying_pattern(curr_line):
118*9c5db199SXin Li                    curr_line = results_file.readline()
119*9c5db199SXin Li                if curr_line == '':
120*9c5db199SXin Li                    break
121*9c5db199SXin Li                try:
122*9c5db199SXin Li                    time, = _extract_values_before_keywords(curr_line, "up")
123*9c5db199SXin Li
124*9c5db199SXin Li                    # Ignore one line.
125*9c5db199SXin Li                    _ = results_file.readline()
126*9c5db199SXin Li
127*9c5db199SXin Li                    # Get the cpu usage.
128*9c5db199SXin Li                    curr_line = results_file.readline()
129*9c5db199SXin Li                    (cpu_user, cpu_sys, cpu_nice, cpu_idle, io_wait, irq, sirq,
130*9c5db199SXin Li                            steal) = _extract_values_before_keywords(curr_line,
131*9c5db199SXin Li                            "us", "sy", "ni", "id", "wa", "hi", "si", "st")
132*9c5db199SXin Li
133*9c5db199SXin Li                    # Get memory usage.
134*9c5db199SXin Li                    curr_line = results_file.readline()
135*9c5db199SXin Li                    (mem_units, mem_free,
136*9c5db199SXin Li                            mem_used) = _extract_values_before_keywords(
137*9c5db199SXin Li                            curr_line, "Mem", "free", "used")
138*9c5db199SXin Li
139*9c5db199SXin Li                    # Get swap usage.
140*9c5db199SXin Li                    curr_line = results_file.readline()
141*9c5db199SXin Li                    (swap_units, swap_free,
142*9c5db199SXin Li                            swap_used) = _extract_values_before_keywords(
143*9c5db199SXin Li                            curr_line, "Swap", "free", "used")
144*9c5db199SXin Li
145*9c5db199SXin Li                    curr_util_values = ResourceMonitorParsedResult.UtilValues(
146*9c5db199SXin Li                            Time=time, UserCPU=cpu_user,
147*9c5db199SXin Li                            SysCPU=cpu_sys, NCPU=cpu_nice, Idle=cpu_idle,
148*9c5db199SXin Li                            IOWait=io_wait, IRQ=irq, SoftIRQ=sirq, Steal=steal,
149*9c5db199SXin Li                            MemUnits=mem_units, UsedMem=mem_used,
150*9c5db199SXin Li                            FreeMem=mem_free,
151*9c5db199SXin Li                            SwapUnits=swap_units, UsedSwap=swap_used,
152*9c5db199SXin Li                            FreeSwap=swap_free)
153*9c5db199SXin Li                    self._utils_over_time.append(curr_util_values)
154*9c5db199SXin Li                except IncorrectTopFormat:
155*9c5db199SXin Li                    logging.error(
156*9c5db199SXin Li                            "Top output format incorrect. Aborting parse.")
157*9c5db199SXin Li                    return
158*9c5db199SXin Li
159*9c5db199SXin Li
160*9c5db199SXin Li    def __repr__(self):
161*9c5db199SXin Li        output_stringfile = six.StringIO()
162*9c5db199SXin Li        self.save_to_file(output_stringfile)
163*9c5db199SXin Li        return output_stringfile.getvalue()
164*9c5db199SXin Li
165*9c5db199SXin Li
166*9c5db199SXin Li    def save_to_file(self, file):
167*9c5db199SXin Li        """Save parsed top results to file
168*9c5db199SXin Li
169*9c5db199SXin Li        @param file file object to write to
170*9c5db199SXin Li
171*9c5db199SXin Li        """
172*9c5db199SXin Li        if len(self._utils_over_time) < 1:
173*9c5db199SXin Li            logging.warning("Tried to save parsed results, but they were "
174*9c5db199SXin Li                    "empty. Skipping the save.")
175*9c5db199SXin Li            return
176*9c5db199SXin Li        csvwriter = csv.writer(file, delimiter=',')
177*9c5db199SXin Li        csvwriter.writerow(self._utils_over_time[0]._fields)
178*9c5db199SXin Li        for row in self._utils_over_time:
179*9c5db199SXin Li            csvwriter.writerow(row)
180*9c5db199SXin Li
181*9c5db199SXin Li
182*9c5db199SXin Li    def save_to_filename(self, filename):
183*9c5db199SXin Li        """Save parsed top results to filename
184*9c5db199SXin Li
185*9c5db199SXin Li        @param filename string filepath to write to
186*9c5db199SXin Li
187*9c5db199SXin Li        """
188*9c5db199SXin Li        out_file = open(filename, "wb")
189*9c5db199SXin Li        self.save_to_file(out_file)
190*9c5db199SXin Li        out_file.close()
191*9c5db199SXin Li
192*9c5db199SXin Li
193*9c5db199SXin Liclass ResourceMonitorConfig(object):
194*9c5db199SXin Li    """Defines a single top run."""
195*9c5db199SXin Li
196*9c5db199SXin Li    DEFAULT_MONITOR_PERIOD = 3
197*9c5db199SXin Li
198*9c5db199SXin Li    def __init__(self, monitor_period=DEFAULT_MONITOR_PERIOD,
199*9c5db199SXin Li            rawresult_output_filename=None):
200*9c5db199SXin Li        """Construct a ResourceMonitorConfig.
201*9c5db199SXin Li
202*9c5db199SXin Li        @param monitor_period float seconds between successive top refreshes.
203*9c5db199SXin Li        @param rawresult_output_filename string filename to output the raw top
204*9c5db199SXin Li                                                results to
205*9c5db199SXin Li
206*9c5db199SXin Li        """
207*9c5db199SXin Li        if monitor_period < 0.1:
208*9c5db199SXin Li            logging.info('Monitor period must be at least 0.1s.'
209*9c5db199SXin Li                    ' Given: %r. Defaulting to 0.1s', monitor_period)
210*9c5db199SXin Li            monitor_period = 0.1
211*9c5db199SXin Li
212*9c5db199SXin Li        self._monitor_period = monitor_period
213*9c5db199SXin Li        self._server_outfile = rawresult_output_filename
214*9c5db199SXin Li
215*9c5db199SXin Li
216*9c5db199SXin Liclass ResourceMonitor(object):
217*9c5db199SXin Li    """Delegate to run top on a client.
218*9c5db199SXin Li
219*9c5db199SXin Li    Usage example (call from a test):
220*9c5db199SXin Li    rmc = resource_monitor.ResourceMonitorConfig(monitor_period=1,
221*9c5db199SXin Li            rawresult_output_filename=os.path.join(self.resultsdir,
222*9c5db199SXin Li                                                    'topout.txt'))
223*9c5db199SXin Li    with resource_monitor.ResourceMonitor(self.context.client.host, rmc) as rm:
224*9c5db199SXin Li        rm.start()
225*9c5db199SXin Li        <operation_to_monitor>
226*9c5db199SXin Li        rm_raw_res = rm.stop()
227*9c5db199SXin Li        rm_res = rm_raw_res.get_parsed_results()
228*9c5db199SXin Li        rm_res.save_to_filename(
229*9c5db199SXin Li                os.path.join(self.resultsdir, 'resource_mon.csv'))
230*9c5db199SXin Li
231*9c5db199SXin Li    """
232*9c5db199SXin Li
233*9c5db199SXin Li    def __init__(self, client_host, config):
234*9c5db199SXin Li        """Construct a ResourceMonitor.
235*9c5db199SXin Li
236*9c5db199SXin Li        @param client_host: SSHHost object representing a remote ssh host
237*9c5db199SXin Li
238*9c5db199SXin Li        """
239*9c5db199SXin Li        self._client_host = client_host
240*9c5db199SXin Li        self._config = config
241*9c5db199SXin Li        self._command_top = path_utils.must_be_installed(
242*9c5db199SXin Li                'top', host=self._client_host)
243*9c5db199SXin Li        self._top_pid = None
244*9c5db199SXin Li
245*9c5db199SXin Li
246*9c5db199SXin Li    def __enter__(self):
247*9c5db199SXin Li        return self
248*9c5db199SXin Li
249*9c5db199SXin Li
250*9c5db199SXin Li    def __exit__(self, exc_type, exc_value, traceback):
251*9c5db199SXin Li        if self._top_pid is not None:
252*9c5db199SXin Li            self._client_host.run('kill %s && rm %s' %
253*9c5db199SXin Li                    (self._top_pid, self._client_outfile), ignore_status=True)
254*9c5db199SXin Li        return True
255*9c5db199SXin Li
256*9c5db199SXin Li
257*9c5db199SXin Li    def start(self):
258*9c5db199SXin Li        """Run top and save results to a temp file on the client."""
259*9c5db199SXin Li        if self._top_pid is not None:
260*9c5db199SXin Li            logging.debug("Tried to start monitoring before stopping. "
261*9c5db199SXin Li                    "Ignoring request.")
262*9c5db199SXin Li            return
263*9c5db199SXin Li
264*9c5db199SXin Li        # Decide where to write top's output to (on the client).
265*9c5db199SXin Li        random_suffix = random.random()
266*9c5db199SXin Li        self._client_outfile = '/tmp/topcap-%r' % random_suffix
267*9c5db199SXin Li
268*9c5db199SXin Li        # Run top on the client.
269*9c5db199SXin Li        top_command = '%s -b -d%d > %s' % (self._command_top,
270*9c5db199SXin Li                self._config._monitor_period, self._client_outfile)
271*9c5db199SXin Li        logging.info('Running top.')
272*9c5db199SXin Li        self._top_pid = self._client_host.run_background(top_command)
273*9c5db199SXin Li        logging.info('Top running with pid %s', self._top_pid)
274*9c5db199SXin Li
275*9c5db199SXin Li
276*9c5db199SXin Li    def stop(self):
277*9c5db199SXin Li        """Stop running top and return the results.
278*9c5db199SXin Li
279*9c5db199SXin Li        @return ResourceMonitorRawResult object
280*9c5db199SXin Li
281*9c5db199SXin Li        """
282*9c5db199SXin Li        logging.debug("Stopping monitor")
283*9c5db199SXin Li        if self._top_pid is None:
284*9c5db199SXin Li            logging.debug("Tried to stop monitoring before starting. "
285*9c5db199SXin Li                    "Ignoring request.")
286*9c5db199SXin Li            return
287*9c5db199SXin Li
288*9c5db199SXin Li        # Stop top on the client.
289*9c5db199SXin Li        self._client_host.run('kill %s' % self._top_pid, ignore_status=True)
290*9c5db199SXin Li
291*9c5db199SXin Li        # Get the top output file from the client onto the server.
292*9c5db199SXin Li        if self._config._server_outfile is None:
293*9c5db199SXin Li            self._config._server_outfile = self._client_outfile
294*9c5db199SXin Li        self._client_host.get_file(
295*9c5db199SXin Li                self._client_outfile, self._config._server_outfile)
296*9c5db199SXin Li
297*9c5db199SXin Li        # Delete the top output file from client.
298*9c5db199SXin Li        self._client_host.run('rm %s' % self._client_outfile,
299*9c5db199SXin Li                ignore_status=True)
300*9c5db199SXin Li
301*9c5db199SXin Li        self._top_pid = None
302*9c5db199SXin Li        logging.info("Saved resource monitor results at %s",
303*9c5db199SXin Li                self._config._server_outfile)
304*9c5db199SXin Li        return ResourceMonitorRawResult(self._config._server_outfile)
305