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 | ||
24 | _METRIC = { | |
25 | "User time (seconds)": float, | |
26 | "System time (seconds)": float, | |
27 | "Percent of CPU this job got": percent_parser, | |
28 | "Elapsed (wall clock) time (h:mm:ss or m:ss)": wall_clock_parser, | |
29 | "Average shared text size (kbytes)": int, | |
30 | "Average unshared data size (kbytes)": int, | |
31 | "Average stack size (kbytes)": int, | |
32 | "Average total size (kbytes)": int, | |
33 | "Maximum resident set size (kbytes)": int, | |
34 | "Average resident set size (kbytes)": int, | |
35 | "Major (requiring I/O) page faults": int, | |
36 | "Minor (reclaiming a frame) page faults": int, | |
37 | "Voluntary context switches": int, | |
38 | "Involuntary context switches": int, | |
39 | "Swaps": int, | |
40 | "File system inputs": int, | |
41 | "File system outputs": int, | |
42 | "Socket messages sent": int, | |
43 | "Socket messages received": int, | |
44 | "Signals delivered": int, | |
45 | "Page size (bytes)": int, | |
46 | } | |
47 | ||
48 | ||
49 | def wall_clock_parser(value): | |
50 | """ | |
51 | Parse /usr/bin/time wall clock value. | |
52 | Wall clock value is expressed in different formats depending on the actual | |
53 | elapsed time. | |
54 | """ | |
55 | total = 0.0 | |
56 | pos = value.find(".") | |
57 | if value.find("."): | |
58 | total += float(value[pos:]) | |
59 | value = value[:pos] | |
60 | ||
61 | v_split = value.split(":") | |
62 | if len(v_split) == 2: | |
63 | total += float(v_split[0]) * 60.0 | |
64 | total += float(v_split[1]) * 1.0 | |
65 | elif len(v_split) == 3: | |
66 | total += float(v_split[0]) * 360.0 | |
67 | total += float(v_split[1]) * 60.0 | |
68 | total += float(v_split[2]) * 1.0 | |
69 | else: | |
70 | return 0.0 | |
71 | ||
72 | return total | |
73 | ||
74 | ||
75 | def percent_parser(value): | |
76 | """ | |
77 | Parse /usr/bin/time percent value. | |
78 | """ | |
79 | parsed = value.replace("%", "").replace("?", "") | |
80 | if parsed: | |
81 | return float(parsed) | |
82 | return 0 | |
83 | ||
84 | ||
85 | def parse(path, results): | |
86 | """ | |
87 | Parser and accumulator for /usr/bin/time results. | |
88 | """ | |
89 | with open(path, "r") as data: | |
90 | for line in data: | |
91 | if line.rfind(":") == -1: | |
92 | continue | |
93 | key, value = line.lstrip().rsplit(": ") | |
94 | if key in _METRIC: | |
95 | results[key].append(_METRIC[key](value)) | |
96 | ||
97 | return results | |
98 | ||
99 | ||
100 | def save(path, results): | |
101 | """ | |
102 | Save the result in json format to path. | |
103 | """ | |
104 | with open(path, "w") as out: | |
105 | json.dump(results, out, sort_keys=True, indent=4) | |
106 | ||
107 | ||
108 | def run(command, iteration, output, stdout, stderr): | |
109 | """ | |
110 | Run the command throught /usr/bin/time n iterations and parse each result. | |
111 | """ | |
112 | results = defaultdict(list) | |
113 | for i in range(iteration): | |
114 | time_stdout = tempfile.NamedTemporaryFile(delete=False) | |
115 | # We must delete this file later on. | |
116 | time_stdout.close() | |
117 | with open(stdout, "a+") as out, open(stderr, "a+") as err: | |
118 | cmd = "/usr/bin/time -v --output='{}' {}".format(time_stdout.name, command) | |
119 | ret = subprocess.run(cmd, shell=True, stdout=out, stderr=err) | |
120 | if ret.returncode != 0: | |
121 | print("Iteration: {}, Command failed: {}".format(str(i), cmd)) | |
122 | results = parse(time_stdout.name, results) | |
123 | os.remove(time_stdout.name) | |
124 | save(output, results) | |
125 | ||
126 | ||
127 | def main(): | |
128 | """ | |
129 | Run /usr/bin/time N time and collect the result. | |
130 | The resulting json have the following form: | |
131 | { | |
132 | "/usr/bin/time": { | |
133 | "User time (seconds)": [], | |
134 | "System time (seconds)": [], | |
135 | "Percent of CPU this job got": [], | |
136 | "Elapsed (wall clock) time (h:mm:ss or m:ss)": [], | |
137 | "Average shared text size (kbytes)": [], | |
138 | "Average unshared data size (kbytes)": [], | |
139 | "Average stack size (kbytes)": [], | |
140 | "Average total size (kbytes)": [], | |
141 | "Maximum resident set size (kbytes)": [], | |
142 | "Average resident set size (kbytes)": [], | |
143 | "Major (requiring I/O) page faults": [], | |
144 | "Minor (reclaiming a frame) page faults": [], | |
145 | "Voluntary context switches": [], | |
146 | "Involuntary context switches": [], | |
147 | "Swaps": [], | |
148 | "File system inputs": [], | |
149 | "File system outputs": [], | |
150 | "Socket messages sent": [], | |
151 | "Socket messages received": [], | |
152 | "Signals delivered": [], | |
153 | "Page size (bytes)": [], | |
154 | } | |
155 | } | |
156 | """ | |
157 | parser = argparse.ArgumentParser( | |
158 | description="Run command N time using /usr/bin/time and collect the statistics" | |
159 | ) | |
160 | parser.add_argument("--output", help="Where to same the result", required=True) | |
161 | parser.add_argument("--command", help="The command to benchmark", required=True) | |
162 | parser.add_argument( | |
163 | "--iteration", | |
164 | type=int, | |
165 | default=5, | |
166 | help="The number of iteration to run the command (default: 5)", | |
167 | required=True, | |
168 | ) | |
169 | parser.add_argument( | |
170 | "--stdout", | |
171 | default="/dev/null", | |
172 | help="Where to append the stdout of each command (default: /dev/null)", | |
173 | ) | |
174 | parser.add_argument( | |
175 | "--stderr", | |
176 | default=os.path.join(os.getcwd(), "stderr.out"), | |
177 | help="Where to append the stderr of each command (default: $CWD/stderr.out)", | |
178 | ) | |
179 | ||
180 | args = parser.parse_args() | |
181 | run(args.command, args.iteration, args.output, args.stdout, args.stderr) | |
182 | ||
183 | ||
184 | if __name__ == "__main__": | |
185 | main() |