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