Total Complexity | 52 |
Total Lines | 239 |
Duplicated Lines | 14.64 % |
Changes | 16 | ||
Bugs | 0 | Features | 0 |
Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like BenchmarkSession often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
1 | from __future__ import division |
||
25 | class BenchmarkSession(object): |
||
26 | compared_mapping = None |
||
27 | groups = None |
||
28 | |||
29 | def __init__(self, config): |
||
30 | self.verbose = config.getoption("benchmark_verbose") |
||
31 | self.logger = Logger(self.verbose, config) |
||
32 | self.config = config |
||
33 | self.performance_regressions = [] |
||
34 | self.benchmarks = [] |
||
35 | self.machine_id = get_machine_id() |
||
36 | |||
37 | self.options = dict( |
||
38 | min_time=SecondsDecimal(config.getoption("benchmark_min_time")), |
||
39 | min_rounds=config.getoption("benchmark_min_rounds"), |
||
40 | max_time=SecondsDecimal(config.getoption("benchmark_max_time")), |
||
41 | timer=load_timer(config.getoption("benchmark_timer")), |
||
42 | calibration_precision=config.getoption("benchmark_calibration_precision"), |
||
43 | disable_gc=config.getoption("benchmark_disable_gc"), |
||
44 | warmup=config.getoption("benchmark_warmup"), |
||
45 | warmup_iterations=config.getoption("benchmark_warmup_iterations"), |
||
46 | use_cprofile=bool(config.getoption("benchmark_cprofile")), |
||
47 | ) |
||
48 | self.skip = config.getoption("benchmark_skip") |
||
49 | self.disabled = config.getoption("benchmark_disable") and not config.getoption("benchmark_enable") |
||
50 | self.cprofile_sort_by = config.getoption("benchmark_cprofile") |
||
51 | |||
52 | if config.getoption("dist", "no") != "no" and not self.skip: |
||
53 | self.logger.warn( |
||
54 | "BENCHMARK-U2", |
||
55 | "Benchmarks are automatically disabled because xdist plugin is active." |
||
56 | "Benchmarks cannot be performed reliably in a parallelized environment.", |
||
57 | fslocation="::" |
||
58 | ) |
||
59 | self.disabled = True |
||
60 | if hasattr(config, "slaveinput"): |
||
61 | self.disabled = True |
||
62 | if not statistics: |
||
63 | self.logger.warn( |
||
64 | "BENCHMARK-U3", |
||
65 | "Benchmarks are automatically disabled because we could not import `statistics`\n\n%s" % |
||
66 | statistics_error, |
||
67 | fslocation="::" |
||
68 | ) |
||
69 | self.disabled = True |
||
70 | |||
71 | self.only = config.getoption("benchmark_only") |
||
72 | self.sort = config.getoption("benchmark_sort") |
||
73 | self.columns = config.getoption("benchmark_columns") |
||
74 | if self.skip and self.only: |
||
75 | raise pytest.UsageError("Can't have both --benchmark-only and --benchmark-skip options.") |
||
76 | if self.disabled and self.only: |
||
77 | raise pytest.UsageError( |
||
78 | "Can't have both --benchmark-only and --benchmark-disable options. Note that --benchmark-disable is " |
||
79 | "automatically activated if xdist is on or you're missing the statistics dependency.") |
||
80 | self.group_by = config.getoption("benchmark_group_by") |
||
81 | self.save = config.getoption("benchmark_save") |
||
82 | self.autosave = config.getoption("benchmark_autosave") |
||
83 | self.save_data = config.getoption("benchmark_save_data") |
||
84 | self.json = config.getoption("benchmark_json") |
||
85 | self.compare = config.getoption("benchmark_compare") |
||
86 | self.compare_fail = config.getoption("benchmark_compare_fail") |
||
87 | self.name_format = NAME_FORMATTERS[config.getoption("benchmark_name")] |
||
88 | |||
89 | self.storage = Storage(config.getoption("benchmark_storage"), |
||
90 | default_machine_id=self.machine_id, logger=self.logger) |
||
91 | self.histogram = first_or_value(config.getoption("benchmark_histogram"), False) |
||
92 | |||
93 | @cached_property |
||
94 | def machine_info(self): |
||
95 | obj = self.config.hook.pytest_benchmark_generate_machine_info(config=self.config) |
||
96 | self.config.hook.pytest_benchmark_update_machine_info( |
||
97 | config=self.config, |
||
98 | machine_info=obj |
||
99 | ) |
||
100 | return obj |
||
101 | |||
102 | def prepare_benchmarks(self): |
||
103 | for bench in self.benchmarks: |
||
104 | if bench: |
||
105 | compared = False |
||
106 | for path, compared_mapping in self.compared_mapping.items(): |
||
107 | if bench.fullname in compared_mapping: |
||
108 | compared = compared_mapping[bench.fullname] |
||
109 | source = short_filename(path, self.machine_id) |
||
110 | flat_bench = bench.as_dict(include_data=False, stats=False, cprofile=self.cprofile_sort_by) |
||
111 | flat_bench.update(compared["stats"]) |
||
112 | flat_bench["path"] = str(path) |
||
113 | flat_bench["source"] = source |
||
114 | if self.compare_fail: |
||
115 | for check in self.compare_fail: |
||
116 | fail = check.fails(bench, flat_bench) |
||
117 | if fail: |
||
118 | self.performance_regressions.append((self.name_format(flat_bench), fail)) |
||
119 | yield flat_bench |
||
120 | flat_bench = bench.as_dict(include_data=False, flat=True, cprofile=self.cprofile_sort_by) |
||
121 | flat_bench["path"] = None |
||
122 | flat_bench["source"] = compared and "NOW" |
||
123 | yield flat_bench |
||
124 | |||
125 | @property |
||
126 | def next_num(self): |
||
127 | files = self.storage.query("[0-9][0-9][0-9][0-9]_*") |
||
128 | files.sort(reverse=True) |
||
129 | if not files: |
||
130 | return "0001" |
||
131 | for f in files: |
||
132 | try: |
||
133 | return "%04i" % (int(str(f.name).split('_')[0]) + 1) |
||
134 | except ValueError: |
||
135 | raise |
||
136 | |||
137 | def handle_saving(self): |
||
138 | save = self.benchmarks and self.save or self.autosave |
||
139 | if save or self.json: |
||
140 | commit_info = self.config.hook.pytest_benchmark_generate_commit_info(config=self.config) |
||
141 | self.config.hook.pytest_benchmark_update_commit_info(config=self.config, commit_info=commit_info) |
||
142 | |||
143 | View Code Duplication | if self.json: |
|
|
|||
144 | output_json = self.config.hook.pytest_benchmark_generate_json( |
||
145 | config=self.config, |
||
146 | benchmarks=self.benchmarks, |
||
147 | include_data=True, |
||
148 | machine_info=self.machine_info, |
||
149 | commit_info=commit_info, |
||
150 | ) |
||
151 | self.config.hook.pytest_benchmark_update_json( |
||
152 | config=self.config, |
||
153 | benchmarks=self.benchmarks, |
||
154 | output_json=output_json, |
||
155 | ) |
||
156 | with self.json as fh: |
||
157 | fh.write(safe_dumps(output_json, ensure_ascii=True, indent=4).encode()) |
||
158 | self.logger.info("Wrote benchmark data in: %s" % self.json, purple=True) |
||
159 | |||
160 | View Code Duplication | if save: |
|
161 | output_json = self.config.hook.pytest_benchmark_generate_json( |
||
162 | config=self.config, |
||
163 | benchmarks=self.benchmarks, |
||
164 | include_data=self.save_data, |
||
165 | machine_info=self.machine_info, |
||
166 | commit_info=commit_info, |
||
167 | ) |
||
168 | self.config.hook.pytest_benchmark_update_json( |
||
169 | config=self.config, |
||
170 | benchmarks=self.benchmarks, |
||
171 | output_json=output_json, |
||
172 | ) |
||
173 | output_file = self.storage.get("%s_%s.json" % (self.next_num, save)) |
||
174 | assert not output_file.exists() |
||
175 | |||
176 | with output_file.open('wb') as fh: |
||
177 | fh.write(safe_dumps(output_json, ensure_ascii=True, indent=4).encode()) |
||
178 | self.logger.info("Saved benchmark data in: %s" % output_file) |
||
179 | |||
180 | def handle_loading(self): |
||
181 | self.compared_mapping = {} |
||
182 | if self.compare: |
||
183 | if self.compare is True: |
||
184 | compared_benchmarks = list(self.storage.load('[0-9][0-9][0-9][0-9]_'))[-1:] |
||
185 | else: |
||
186 | compared_benchmarks = list(self.storage.load(self.compare)) |
||
187 | |||
188 | if not compared_benchmarks: |
||
189 | msg = "Can't compare. No benchmark files in %r" % str(self.storage) |
||
190 | if self.compare is True: |
||
191 | msg += ". Can't load the previous benchmark." |
||
192 | code = "BENCHMARK-C2" |
||
193 | else: |
||
194 | msg += " match %r." % self.compare |
||
195 | code = "BENCHMARK-C1" |
||
196 | self.logger.warn(code, msg, fslocation=self.storage.location) |
||
197 | |||
198 | for path, compared_benchmark in compared_benchmarks: |
||
199 | self.config.hook.pytest_benchmark_compare_machine_info( |
||
200 | config=self.config, |
||
201 | benchmarksession=self, |
||
202 | machine_info=self.machine_info, |
||
203 | compared_benchmark=compared_benchmark, |
||
204 | ) |
||
205 | self.compared_mapping[path] = dict( |
||
206 | (bench['fullname'], bench) for bench in compared_benchmark['benchmarks'] |
||
207 | ) |
||
208 | self.logger.info("Comparing against benchmarks from: %s" % path) |
||
209 | |||
210 | def finish(self): |
||
211 | self.handle_saving() |
||
212 | self.handle_loading() |
||
213 | prepared_benchmarks = list(self.prepare_benchmarks()) |
||
214 | if prepared_benchmarks: |
||
215 | self.groups = self.config.hook.pytest_benchmark_group_stats( |
||
216 | config=self.config, |
||
217 | benchmarks=prepared_benchmarks, |
||
218 | group_by=self.group_by |
||
219 | ) |
||
220 | |||
221 | def display(self, tr): |
||
222 | if not self.groups: |
||
223 | return |
||
224 | |||
225 | tr.ensure_newline() |
||
226 | results_table = TableResults( |
||
227 | columns=self.columns, |
||
228 | sort=self.sort, |
||
229 | histogram=self.histogram, |
||
230 | name_format=self.name_format, |
||
231 | logger=self.logger |
||
232 | ) |
||
233 | results_table.display(tr, self.groups) |
||
234 | self.check_regressions() |
||
235 | self.display_cprofile(tr) |
||
236 | |||
237 | def check_regressions(self): |
||
238 | if self.compare_fail and not self.compared_mapping: |
||
239 | raise pytest.UsageError("--benchmark-compare-fail requires valid --benchmark-compare.") |
||
240 | |||
241 | if self.performance_regressions: |
||
242 | self.logger.error("Performance has regressed:\n%s" % "\n".join( |
||
243 | "\t%s - %s" % line for line in self.performance_regressions |
||
244 | )) |
||
245 | raise PerformanceRegression("Performance has regressed.") |
||
246 | |||
247 | def display_cprofile(self, tr): |
||
248 | if self.options["use_cprofile"]: |
||
249 | tr.section("cProfile information") |
||
250 | tr.write_line("Time in s") |
||
251 | for group in self.groups: |
||
252 | group_name, benchmarks = group |
||
253 | for benchmark in benchmarks: |
||
254 | tr.write(benchmark["fullname"], yellow=True) |
||
255 | if benchmark["source"]: |
||
256 | tr.write_line(" ({})".format((benchmark["source"]))) |
||
257 | else: |
||
258 | tr.write("\n") |
||
259 | tr.write_line("ncalls\ttottime\tpercall\tcumtime\tpercall\tfilename:lineno(function)") |
||
260 | for function_info in benchmark["cprofile"]: |
||
261 | line = "{ncalls_recursion}\t{tottime:.{prec}f}\t{tottime_per:.{prec}f}\t{cumtime:.{prec}f}\t{cumtime_per:.{prec}f}\t{function_name}".format(prec=4, **function_info) |
||
262 | tr.write_line(line) |
||
263 | tr.write("\n") |
||
264 |