Commit | Line | Data |
---|---|---|
5c65bbc2 JR |
1 | #!/usr/bin/python3 |
2 | # Copyright (C) 2019 - Jonathan Rajotte Julien <jonathan.rajotte-julien@efficios.com> | |
3 | # | |
4 | # This program is free software: you can redistribute it and/or modify | |
5 | # it under the terms of the GNU General Public License as published by | |
6 | # the Free Software Foundation, either version 3 of the License, or | |
7 | # (at your option) any later version. | |
8 | # | |
9 | # This program is distributed in the hope that it will be useful, | |
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
12 | # GNU General Public License for more details. | |
13 | # | |
14 | # You should have received a copy of the GNU General Public License | |
15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
16 | ||
17 | import argparse | |
18 | import os | |
19 | import subprocess | |
20 | import tempfile | |
21 | import json | |
22 | from collections import defaultdict | |
23 | ||
5c65bbc2 JR |
24 | def wall_clock_parser(value): |
25 | """ | |
26 | Parse /usr/bin/time wall clock value. | |
27 | Wall clock value is expressed in different formats depending on the actual | |
28 | elapsed time. | |
29 | """ | |
30 | total = 0.0 | |
31 | pos = value.find(".") | |
32 | if value.find("."): | |
33 | total += float(value[pos:]) | |
34 | value = value[:pos] | |
35 | ||
36 | v_split = value.split(":") | |
37 | if len(v_split) == 2: | |
38 | total += float(v_split[0]) * 60.0 | |
39 | total += float(v_split[1]) * 1.0 | |
40 | elif len(v_split) == 3: | |
41 | total += float(v_split[0]) * 360.0 | |
42 | total += float(v_split[1]) * 60.0 | |
43 | total += float(v_split[2]) * 1.0 | |
44 | else: | |
45 | return 0.0 | |
46 | ||
47 | return total | |
48 | ||
49 | ||
50 | def percent_parser(value): | |
51 | """ | |
52 | Parse /usr/bin/time percent value. | |
53 | """ | |
54 | parsed = value.replace("%", "").replace("?", "") | |
55 | if parsed: | |
56 | return float(parsed) | |
57 | return 0 | |
58 | ||
59 | ||
9ade27c5 JR |
60 | _METRIC = { |
61 | "User time (seconds)": float, | |
62 | "System time (seconds)": float, | |
63 | "Percent of CPU this job got": percent_parser, | |
64 | "Elapsed (wall clock) time (h:mm:ss or m:ss)": wall_clock_parser, | |
65 | "Average shared text size (kbytes)": int, | |
66 | "Average unshared data size (kbytes)": int, | |
67 | "Average stack size (kbytes)": int, | |
68 | "Average total size (kbytes)": int, | |
69 | "Maximum resident set size (kbytes)": int, | |
70 | "Average resident set size (kbytes)": int, | |
71 | "Major (requiring I/O) page faults": int, | |
72 | "Minor (reclaiming a frame) page faults": int, | |
73 | "Voluntary context switches": int, | |
74 | "Involuntary context switches": int, | |
75 | "Swaps": int, | |
76 | "File system inputs": int, | |
77 | "File system outputs": int, | |
78 | "Socket messages sent": int, | |
79 | "Socket messages received": int, | |
80 | "Signals delivered": int, | |
81 | "Page size (bytes)": int, | |
82 | } | |
83 | ||
84 | ||
85 | ||
5c65bbc2 JR |
86 | def parse(path, results): |
87 | """ | |
88 | Parser and accumulator for /usr/bin/time results. | |
89 | """ | |
90 | with open(path, "r") as data: | |
91 | for line in data: | |
92 | if line.rfind(":") == -1: | |
93 | continue | |
94 | key, value = line.lstrip().rsplit(": ") | |
95 | if key in _METRIC: | |
96 | results[key].append(_METRIC[key](value)) | |
97 | ||
98 | return results | |
99 | ||
100 | ||
101 | def save(path, results): | |
102 | """ | |
103 | Save the result in json format to path. | |
104 | """ | |
105 | with open(path, "w") as out: | |
106 | json.dump(results, out, sort_keys=True, indent=4) | |
107 | ||
108 | ||
109 | def run(command, iteration, output, stdout, stderr): | |
110 | """ | |
111 | Run the command throught /usr/bin/time n iterations and parse each result. | |
112 | """ | |
113 | results = defaultdict(list) | |
114 | for i in range(iteration): | |
115 | time_stdout = tempfile.NamedTemporaryFile(delete=False) | |
116 | # We must delete this file later on. | |
117 | time_stdout.close() | |
118 | with open(stdout, "a+") as out, open(stderr, "a+") as err: | |
119 | cmd = "/usr/bin/time -v --output='{}' {}".format(time_stdout.name, command) | |
120 | ret = subprocess.run(cmd, shell=True, stdout=out, stderr=err) | |
121 | if ret.returncode != 0: | |
122 | print("Iteration: {}, Command failed: {}".format(str(i), cmd)) | |
123 | results = parse(time_stdout.name, results) | |
124 | os.remove(time_stdout.name) | |
125 | save(output, results) | |
126 | ||
127 | ||
128 | def main(): | |
129 | """ | |
130 | Run /usr/bin/time N time and collect the result. | |
131 | The resulting json have the following form: | |
132 | { | |
133 | "/usr/bin/time": { | |
134 | "User time (seconds)": [], | |
135 | "System time (seconds)": [], | |
136 | "Percent of CPU this job got": [], | |
137 | "Elapsed (wall clock) time (h:mm:ss or m:ss)": [], | |
138 | "Average shared text size (kbytes)": [], | |
139 | "Average unshared data size (kbytes)": [], | |
140 | "Average stack size (kbytes)": [], | |
141 | "Average total size (kbytes)": [], | |
142 | "Maximum resident set size (kbytes)": [], | |
143 | "Average resident set size (kbytes)": [], | |
144 | "Major (requiring I/O) page faults": [], | |
145 | "Minor (reclaiming a frame) page faults": [], | |
146 | "Voluntary context switches": [], | |
147 | "Involuntary context switches": [], | |
148 | "Swaps": [], | |
149 | "File system inputs": [], | |
150 | "File system outputs": [], | |
151 | "Socket messages sent": [], | |
152 | "Socket messages received": [], | |
153 | "Signals delivered": [], | |
154 | "Page size (bytes)": [], | |
155 | } | |
156 | } | |
157 | """ | |
158 | parser = argparse.ArgumentParser( | |
159 | description="Run command N time using /usr/bin/time and collect the statistics" | |
160 | ) | |
161 | parser.add_argument("--output", help="Where to same the result", required=True) | |
162 | parser.add_argument("--command", help="The command to benchmark", required=True) | |
163 | parser.add_argument( | |
164 | "--iteration", | |
165 | type=int, | |
166 | default=5, | |
167 | help="The number of iteration to run the command (default: 5)", | |
168 | required=True, | |
169 | ) | |
170 | parser.add_argument( | |
171 | "--stdout", | |
172 | default="/dev/null", | |
173 | help="Where to append the stdout of each command (default: /dev/null)", | |
174 | ) | |
175 | parser.add_argument( | |
176 | "--stderr", | |
177 | default=os.path.join(os.getcwd(), "stderr.out"), | |
178 | help="Where to append the stderr of each command (default: $CWD/stderr.out)", | |
179 | ) | |
180 | ||
181 | args = parser.parse_args() | |
182 | run(args.command, args.iteration, args.output, args.stdout, args.stderr) | |
183 | ||
184 | ||
185 | if __name__ == "__main__": | |
186 | main() |