feat(bisect): introduce scripts for VPP bisecting
[csit.git] / resources / tools / integrated / compare_perpatch.py
1 # Copyright (c) 2023 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Script for determining whether per-patch perf test votes -1.
15
16 This script expects a particular tree created on a filesystem by
17 per_patch_perf.sh bootstrap script, including test results
18 exported as json files according to a current model schema.
19 This script extracts the results (according to result type)
20 and joins them into one list of floats for parent and one for current.
21
22 This script then uses jumpavg library to determine whether there was
23 a regression, progression or no change for each testcase.
24
25 If the set of test names does not match, or there was a regression,
26 this script votes -1 (by exiting with code 1), otherwise it votes +1 (exit 0).
27 """
28
29 import sys
30
31 from resources.libraries.python import jumpavg
32 from resources.libraries.python.model.parse import parse
33
34
35 def main() -> int:
36     """Execute the main logic, return a number to return as the return code.
37
38     Call parse to get parent and current data.
39     Use higher fake value for parent, so changes that keep a test failing
40     are marked as regressions.
41
42     If there are multiple iterations, the value lists are joined.
43     For each test, call jumpavg.classify to detect possible regression.
44
45     If there is at least one regression, return 3.
46
47     :returns: Return code, 0 or 3 based on the comparison result.
48     :rtype: int
49     """
50     iteration = -1
51     parent_aggregate = {}
52     current_aggregate = {}
53     test_names = None
54     while 1:
55         iteration += 1
56         parent_results = {}
57         current_results = {}
58         parent_results = parse(f"csit_parent/{iteration}", fake_value=2.0)
59         parent_names = set(parent_results.keys())
60         if test_names is None:
61             test_names = parent_names
62         if not parent_names:
63             # No more iterations.
64             break
65         assert parent_names == test_names, f"{parent_names} != {test_names}"
66         current_results = parse(f"csit_current/{iteration}", fake_value=1.0)
67         current_names = set(current_results.keys())
68         assert (
69             current_names == parent_names
70         ), f"{current_names} != {parent_names}"
71         for name in test_names:
72             if name not in parent_aggregate:
73                 parent_aggregate[name] = []
74             if name not in current_aggregate:
75                 current_aggregate[name] = []
76             parent_aggregate[name].extend(parent_results[name])
77             current_aggregate[name].extend(current_results[name])
78     exit_code = 0
79     for name in test_names:
80         print(f"Test name: {name}")
81         parent_values = parent_aggregate[name]
82         current_values = current_aggregate[name]
83         print(f"Time-ordered MRR values for parent build: {parent_values}")
84         print(f"Time-ordered MRR values for current build: {current_values}")
85         parent_values = sorted(parent_values)
86         current_values = sorted(current_values)
87         max_value = max([1.0] + parent_values + current_values)
88         parent_stats = jumpavg.AvgStdevStats.for_runs(parent_values)
89         current_stats = jumpavg.AvgStdevStats.for_runs(current_values)
90         parent_group_list = jumpavg.BitCountingGroupList(
91             max_value=max_value
92         ).append_group_of_runs([parent_stats])
93         combined_group_list = (
94             parent_group_list.copy().extend_runs_to_last_group([current_stats])
95         )
96         separated_group_list = parent_group_list.append_group_of_runs(
97             [current_stats]
98         )
99         print(f"Value-ordered MRR values for parent build: {parent_values}")
100         print(f"Value-ordered MRR values for current build: {current_values}")
101         avg_diff = (current_stats.avg - parent_stats.avg) / parent_stats.avg
102         print(f"Difference of averages relative to parent: {100 * avg_diff}%")
103         print(f"Jumpavg representation of parent group: {parent_stats}")
104         print(f"Jumpavg representation of current group: {current_stats}")
105         print(
106             f"Jumpavg representation of both as one group:"
107             f" {combined_group_list[0].stats}"
108         )
109         bits_diff = separated_group_list.bits - combined_group_list.bits
110         compared = "longer" if bits_diff >= 0 else "shorter"
111         print(
112             f"Separate groups are {compared} than single group"
113             f" by {abs(bits_diff)} bits"
114         )
115         # TODO: Version of classify that takes max_value and list of stats?
116         # That matters if only stats (not list of floats) are given.
117         classified_list = jumpavg.classify([parent_values, current_values])
118         if len(classified_list) < 2:
119             print(f"Test {name}: normal (no anomaly)")
120             continue
121         anomaly = classified_list[1].comment
122         if anomaly == "regression":
123             print(f"Test {name}: anomaly regression")
124             exit_code = 3  # 1 or 2 can be caused by other errors
125             continue
126         print(f"Test {name}: anomaly {anomaly}")
127     print(f"Exit code: {exit_code}")
128     return exit_code
129
130
131 if __name__ == "__main__":
132     sys.exit(main())