1*4882a593Smuzhiyun#!/usr/bin/env python3 2*4882a593Smuzhiyun# 3*4882a593Smuzhiyun# SPDX-License-Identifier: GPL-2.0-only 4*4882a593Smuzhiyun# 5*4882a593Smuzhiyun 6*4882a593Smuzhiyunimport argparse 7*4882a593Smuzhiyunimport os 8*4882a593Smuzhiyunimport re 9*4882a593Smuzhiyunimport sys 10*4882a593Smuzhiyun 11*4882a593Smuzhiyunarg_parser = argparse.ArgumentParser( 12*4882a593Smuzhiyun description=""" 13*4882a593SmuzhiyunReports time consumed for one or more task in a format similar to the standard 14*4882a593SmuzhiyunBash 'time' builtin. Optionally sorts tasks by real (wall-clock), user (user 15*4882a593Smuzhiyunspace CPU), or sys (kernel CPU) time. 16*4882a593Smuzhiyun""") 17*4882a593Smuzhiyun 18*4882a593Smuzhiyunarg_parser.add_argument( 19*4882a593Smuzhiyun "paths", 20*4882a593Smuzhiyun metavar="path", 21*4882a593Smuzhiyun nargs="+", 22*4882a593Smuzhiyun help=""" 23*4882a593SmuzhiyunA path containing task buildstats. If the path is a directory, e.g. 24*4882a593Smuzhiyunbuild/tmp/buildstats, then all task found (recursively) in it will be 25*4882a593Smuzhiyunprocessed. If the path is a single task buildstat, e.g. 26*4882a593Smuzhiyunbuild/tmp/buildstats/20161018083535/foo-1.0-r0/do_compile, then just that 27*4882a593Smuzhiyunbuildstat will be processed. Multiple paths can be specified to process all of 28*4882a593Smuzhiyunthem. Files whose names do not start with "do_" are ignored. 29*4882a593Smuzhiyun""") 30*4882a593Smuzhiyun 31*4882a593Smuzhiyunarg_parser.add_argument( 32*4882a593Smuzhiyun "--sort", 33*4882a593Smuzhiyun choices=("none", "real", "user", "sys"), 34*4882a593Smuzhiyun default="none", 35*4882a593Smuzhiyun help=""" 36*4882a593SmuzhiyunThe measurement to sort the output by. Defaults to 'none', which means to sort 37*4882a593Smuzhiyunby the order paths were given on the command line. For other options, tasks are 38*4882a593Smuzhiyunsorted in descending order from the highest value. 39*4882a593Smuzhiyun""") 40*4882a593Smuzhiyun 41*4882a593Smuzhiyunargs = arg_parser.parse_args() 42*4882a593Smuzhiyun 43*4882a593Smuzhiyun# Field names and regexes for parsing out their values from buildstat files 44*4882a593Smuzhiyunfield_regexes = (("elapsed", ".*Elapsed time: ([0-9.]+)"), 45*4882a593Smuzhiyun ("user", "rusage ru_utime: ([0-9.]+)"), 46*4882a593Smuzhiyun ("sys", "rusage ru_stime: ([0-9.]+)"), 47*4882a593Smuzhiyun ("child user", "Child rusage ru_utime: ([0-9.]+)"), 48*4882a593Smuzhiyun ("child sys", "Child rusage ru_stime: ([0-9.]+)")) 49*4882a593Smuzhiyun 50*4882a593Smuzhiyun# A list of (<path>, <dict>) tuples, where <path> is the path of a do_* task 51*4882a593Smuzhiyun# buildstat file and <dict> maps fields from the file to their values 52*4882a593Smuzhiyuntask_infos = [] 53*4882a593Smuzhiyun 54*4882a593Smuzhiyundef save_times_for_task(path): 55*4882a593Smuzhiyun """Saves information for the buildstat file 'path' in 'task_infos'.""" 56*4882a593Smuzhiyun 57*4882a593Smuzhiyun if not os.path.basename(path).startswith("do_"): 58*4882a593Smuzhiyun return 59*4882a593Smuzhiyun 60*4882a593Smuzhiyun with open(path) as f: 61*4882a593Smuzhiyun fields = {} 62*4882a593Smuzhiyun 63*4882a593Smuzhiyun for line in f: 64*4882a593Smuzhiyun for name, regex in field_regexes: 65*4882a593Smuzhiyun match = re.match(regex, line) 66*4882a593Smuzhiyun if match: 67*4882a593Smuzhiyun fields[name] = float(match.group(1)) 68*4882a593Smuzhiyun break 69*4882a593Smuzhiyun 70*4882a593Smuzhiyun # Check that all expected fields were present 71*4882a593Smuzhiyun for name, regex in field_regexes: 72*4882a593Smuzhiyun if name not in fields: 73*4882a593Smuzhiyun print("Warning: Skipping '{}' because no field matching '{}' could be found" 74*4882a593Smuzhiyun .format(path, regex), 75*4882a593Smuzhiyun file=sys.stderr) 76*4882a593Smuzhiyun return 77*4882a593Smuzhiyun 78*4882a593Smuzhiyun task_infos.append((path, fields)) 79*4882a593Smuzhiyun 80*4882a593Smuzhiyundef save_times_for_dir(path): 81*4882a593Smuzhiyun """Runs save_times_for_task() for each file in path and its subdirs, recursively.""" 82*4882a593Smuzhiyun 83*4882a593Smuzhiyun # Raise an exception for os.walk() errors instead of ignoring them 84*4882a593Smuzhiyun def walk_onerror(e): 85*4882a593Smuzhiyun raise e 86*4882a593Smuzhiyun 87*4882a593Smuzhiyun for root, _, files in os.walk(path, onerror=walk_onerror): 88*4882a593Smuzhiyun for fname in files: 89*4882a593Smuzhiyun save_times_for_task(os.path.join(root, fname)) 90*4882a593Smuzhiyun 91*4882a593Smuzhiyunfor path in args.paths: 92*4882a593Smuzhiyun if os.path.isfile(path): 93*4882a593Smuzhiyun save_times_for_task(path) 94*4882a593Smuzhiyun else: 95*4882a593Smuzhiyun save_times_for_dir(path) 96*4882a593Smuzhiyun 97*4882a593Smuzhiyundef elapsed_time(task_info): 98*4882a593Smuzhiyun return task_info[1]["elapsed"] 99*4882a593Smuzhiyun 100*4882a593Smuzhiyundef tot_user_time(task_info): 101*4882a593Smuzhiyun return task_info[1]["user"] + task_info[1]["child user"] 102*4882a593Smuzhiyun 103*4882a593Smuzhiyundef tot_sys_time(task_info): 104*4882a593Smuzhiyun return task_info[1]["sys"] + task_info[1]["child sys"] 105*4882a593Smuzhiyun 106*4882a593Smuzhiyunif args.sort != "none": 107*4882a593Smuzhiyun sort_fn = {"real": elapsed_time, "user": tot_user_time, "sys": tot_sys_time} 108*4882a593Smuzhiyun task_infos.sort(key=sort_fn[args.sort], reverse=True) 109*4882a593Smuzhiyun 110*4882a593Smuzhiyunfirst_entry = True 111*4882a593Smuzhiyun 112*4882a593Smuzhiyun# Catching BrokenPipeError avoids annoying errors when the output is piped into 113*4882a593Smuzhiyun# e.g. 'less' or 'head' and not completely read 114*4882a593Smuzhiyuntry: 115*4882a593Smuzhiyun for task_info in task_infos: 116*4882a593Smuzhiyun real = elapsed_time(task_info) 117*4882a593Smuzhiyun user = tot_user_time(task_info) 118*4882a593Smuzhiyun sys = tot_sys_time(task_info) 119*4882a593Smuzhiyun 120*4882a593Smuzhiyun if not first_entry: 121*4882a593Smuzhiyun print() 122*4882a593Smuzhiyun first_entry = False 123*4882a593Smuzhiyun 124*4882a593Smuzhiyun # Mimic Bash's 'time' builtin 125*4882a593Smuzhiyun print("{}:\n" 126*4882a593Smuzhiyun "real\t{}m{:.3f}s\n" 127*4882a593Smuzhiyun "user\t{}m{:.3f}s\n" 128*4882a593Smuzhiyun "sys\t{}m{:.3f}s" 129*4882a593Smuzhiyun .format(task_info[0], 130*4882a593Smuzhiyun int(real//60), real%60, 131*4882a593Smuzhiyun int(user//60), user%60, 132*4882a593Smuzhiyun int(sys//60), sys%60)) 133*4882a593Smuzhiyun 134*4882a593Smuzhiyunexcept BrokenPipeError: 135*4882a593Smuzhiyun pass 136