33
44Reports from previous runs are stored in the reports folder and can be used for comparison.
55
6- Each report is run multiple times and calculates a trimmed mean by excluding the bottom and top 10% of values.
6+ Each report is run multiple times and calculates a trimmed mean by excluding the bottom and top values (according to
7+ cut_off parameter).
78"""
89
910import json
1617
1718from robocop import __version__ , config
1819from robocop .formatter .formatters import FORMATTERS
20+ from robocop .linter .utils .version_matching import Version
1921from robocop .run import check_files , format_files
2022from tests import working_directory
2123
2224LINTER_TESTS_DIR = Path (__file__ ).parent .parent / "linter"
2325TEST_DATA = Path (__file__ ).parent / "test_data"
26+ ROBOCOP_VERSION = Version (__version__ )
2427REPORTS = {}
2528
2629
27- def performance_report (runs : int = 100 ):
28- """Use as decorator to measure performance of a function and store results."""
30+ def performance_report (runs : int = 100 , cut_off : int = 0 ):
31+ """
32+ Use as decorator to measure performance of a function and store results.
33+
34+ Args:
35+ runs: Number of runs to take into account when calculating the average.
36+ cut_off: Number of slowest and fastest runs to exclude from the average.
37+
38+ """
2939
3040 def decorator (func ):
3141 @wraps (func )
@@ -38,18 +48,13 @@ def wrapper(*args, **kwargs):
3848 print (f"Run { run + 1 } / { runs } of { func .__name__ } " )
3949 start = time .perf_counter ()
4050 counter = func (* args , ** kwargs )
41- end = time .perf_counter ()
42- time_taken = end - start
51+ time_taken = time .perf_counter () - start
4352 run_times .append (time_taken )
4453 print (f" Execution time: { time_taken :.6f} seconds" )
4554 run_times .sort ()
46- cut_off = int (runs * 0.1 )
47- if cut_off + 2 > runs :
48- cut_off = 0
49- if len (run_times ) > 2 :
50- avg_time = sum (run_times [cut_off :- cut_off ]) / (len (run_times ) - 2 * cut_off )
51- else :
52- avg_time = sum (run_times ) / len (run_times )
55+ if cut_off :
56+ run_times = run_times [cut_off :- cut_off ]
57+ avg_time = sum (run_times ) / len (run_times )
5358 print (f"Mean average execution time over { runs } runs: { avg_time :.6f} seconds" )
5459 if report_name :
5560 if func .__name__ not in REPORTS :
@@ -63,7 +68,7 @@ def wrapper(*args, **kwargs):
6368 return decorator
6469
6570
66- @performance_report (runs = 50 )
71+ @performance_report (runs = 10 , cut_off = 2 )
6772def project_traversing_report () -> int :
6873 """
6974 Measure how long it takes to traverse Robocop repository files.
@@ -90,33 +95,41 @@ def project_traversing_report() -> int:
9095 return files_count
9196
9297
93- @performance_report (runs = 50 )
94- def formatter_report (formatter : str , report_name : str , cache : bool = True ) -> int : # noqa: ARG001
98+ @performance_report (runs = 10 , cut_off = 2 )
99+ def formatter_report (formatter : str , report_name : str , ** kwargs ) -> int : # noqa: ARG001
100+ """Measure how long it takes to format test files using a specific formatter."""
95101 main_dir = Path (__file__ ).parent .parent .parent
96102 formatter_dir = main_dir / "tests" / "formatter" / "formatters" / formatter
97103 with working_directory (formatter_dir ):
98- format_files (["source" ], select = [formatter ], overwrite = False , return_result = True , silent = True , cache = cache )
104+ format_files (["source" ], select = [formatter ], overwrite = False , return_result = True , silent = True , ** kwargs )
99105 source_dir = formatter_dir / "source"
100106 return len (list (source_dir .iterdir ()))
101107
102108
103- @performance_report (runs = 10 )
109+ @performance_report (runs = 5 )
104110def linter_report (report_name : str , ** kwargs ) -> int : # noqa: ARG001
111+ """Measure how long it takes to lint all linter test files."""
105112 main_dir = Path (__file__ ).parent .parent .parent
106113 linter_dir = main_dir / "tests" / "linter"
107114 with working_directory (linter_dir ):
108115 check_files (return_result = True , select = ["ALL" ], ** kwargs )
109116 return len (list (linter_dir .glob ("**/*.robot" )))
110117
111118
112- @performance_report (runs = 2 )
119+ @performance_report (runs = 1 )
113120def lint_large_file (report_name : str , lint_dir : Path , ** kwargs ) -> int : # noqa: ARG001
121+ """Measure how long it takes to lint a large file."""
114122 with working_directory (lint_dir ):
115- check_files (return_result = True , select = ["ALL" ], cache = False , ** kwargs )
123+ check_files (return_result = True , select = ["ALL" ], ** kwargs )
116124 return 1
117125
118126
119127def merge_dictionaries (d1 : dict , d2 : dict ) -> dict :
128+ """
129+ Merge two dictionaries recursively.
130+
131+ This function is used to merge two partial reports generated by different runs.
132+ """
120133 for key , value in d2 .items ():
121134 if key in d1 and isinstance (d1 [key ], dict ) and isinstance (value , dict ):
122135 merge_dictionaries (d1 [key ], value )
@@ -126,6 +139,12 @@ def merge_dictionaries(d1: dict, d2: dict) -> dict:
126139
127140
128141def generate_large_file (template_path : Path , output_dir : Path ) -> None :
142+ """
143+ Generate a large file based on a template.
144+
145+ This function is used to generate a large file for performance testing. Because of the potential size and
146+ complexity, it is easier to use a templated file than hardcoded one.
147+ """
129148 env = Environment (loader = FileSystemLoader (template_path .parent ), autoescape = True )
130149 template = env .get_template (template_path .name )
131150
@@ -135,30 +154,37 @@ def generate_large_file(template_path: Path, output_dir: Path) -> None:
135154 f .write (rendered_content )
136155
137156
138- if __name__ == "__main__" :
139- # TODO: prepare i.e. nox script to install external robocops and run this script
140- # So we can generate reports for multiple past versions. It is important since the actual seconds change depending
141- # on where we run the script from, but the % change between version should be comparable. Also we can use new tests
142- # on old versions
143- linter_report (report_name = "with_print_cache" , cache = True )
144- linter_report (report_name = "with_print_no_cache" , cache = False )
145- linter_report (report_name = "without_print_cache" , silent = True , cache = True )
146- linter_report (report_name = "without_print_no_cache" , silent = True , cache = False )
157+ def generate_reports () -> None :
158+ """Entry point for generating performance reports and saving it to global REPORTS variable."""
159+ if Version ("7.1.0" ) > ROBOCOP_VERSION :
160+ disable_cache_option = {}
161+ elif Version ("7.1.0" ) == ROBOCOP_VERSION :
162+ disable_cache_option = {"no_cache" : True }
163+ else :
164+ disable_cache_option = {"cache" : False }
165+
166+ if disable_cache_option :
167+ linter_report (report_name = "with_print_cache" )
168+ linter_report (report_name = "with_print_no_cache" , ** disable_cache_option )
169+ if disable_cache_option :
170+ linter_report (report_name = "without_print_cache" , silent = True )
171+ linter_report (report_name = "without_print_no_cache" , silent = True , ** disable_cache_option )
147172 for formatter in FORMATTERS :
148- formatter_report (formatter = formatter , report_name = formatter )
149- formatter_report (formatter = formatter , report_name = f"{ formatter } _no_cache" , cache = False )
173+ formatter_report (formatter = formatter , report_name = f"{ formatter } _no_cache" , ** disable_cache_option )
150174 project_traversing_report ()
151175 with tempfile .TemporaryDirectory () as temp_dir :
152176 temp_dir = Path (temp_dir )
153177 generate_large_file (TEST_DATA / "large_file.robot" , temp_dir )
154- lint_large_file (report_name = "large_file_with_print" , lint_dir = temp_dir )
155- lint_large_file (report_name = "large_file_without_print" , lint_dir = temp_dir , silent = True )
178+ lint_large_file (report_name = "large_file_with_print" , lint_dir = temp_dir , ** disable_cache_option )
179+ lint_large_file (report_name = "large_file_without_print" , lint_dir = temp_dir , silent = True , ** disable_cache_option )
156180
157- report_path = Path (__file__ ).parent / "reports" / f"robocop_{ __version__ .replace ('.' , '_' )} .json"
158- if report_path .exists ():
159- with open (report_path ) as fp :
160- prev_report = json .load (fp )
161- REPORTS = merge_dictionaries (prev_report , REPORTS )
162181
163- with open (report_path , "w" ) as fp :
164- json .dump (REPORTS , fp , indent = 4 )
182+ if __name__ == "__main__" :
183+ whole_run_start = time .perf_counter ()
184+ report_path = Path (__file__ ).parent / "reports" / f"robocop_{ __version__ .replace ('.' , '_' )} .json"
185+ if not report_path .exists (): # additional safe guard in case we run on the same version (there was no version bump)
186+ generate_reports ()
187+ print (f"Generating report in { report_path } " )
188+ with open (report_path , "w" ) as fp :
189+ json .dump (REPORTS , fp , indent = 4 )
190+ print (f"Took { time .perf_counter () - whole_run_start :.2f} seconds to generate report." )
0 commit comments