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