xref: /aosp_15_r20/external/toolchain-utils/afdo_tools/bisection/afdo_prof_analysis_test.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# Copyright 2019 The ChromiumOS Authors
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""Tests for afdo_prof_analysis."""
8
9
10import random
11import io
12import unittest
13
14from afdo_tools.bisection import afdo_prof_analysis as analysis
15
16
17class AfdoProfAnalysisTest(unittest.TestCase):
18    """Class for testing AFDO Profile Analysis"""
19
20    bad_items = {"func_a": "1", "func_b": "3", "func_c": "5"}
21    good_items = {"func_a": "2", "func_b": "4", "func_d": "5"}
22    random.seed(13)  # 13 is an arbitrary choice. just for consistency
23    # add some extra info to make tests more reflective of real scenario
24    for num in range(128):
25        func_name = "func_extra_%d" % num
26        # 1/3 to both, 1/3 only to good, 1/3 only to bad
27        rand_val = random.randint(1, 101)
28        if rand_val < 67:
29            bad_items[func_name] = "test_data"
30        if rand_val < 34 or rand_val >= 67:
31            good_items[func_name] = "test_data"
32
33    analysis.random.seed(5)  # 5 is an arbitrary choice. For consistent testing
34
35    def test_text_to_json(self):
36        test_data = io.StringIO(
37            "deflate_slow:87460059:3\n"
38            " 3: 24\n"
39            " 14: 54767\n"
40            " 15: 664 fill_window:22\n"
41            " 16: 661\n"
42            " 19: 637\n"
43            " 41: 36692 longest_match:36863\n"
44            " 44: 36692\n"
45            " 44.2: 5861\n"
46            " 46: 13942\n"
47            " 46.1: 14003\n"
48        )
49        expected = {
50            "deflate_slow": ":87460059:3\n"
51            " 3: 24\n"
52            " 14: 54767\n"
53            " 15: 664 fill_window:22\n"
54            " 16: 661\n"
55            " 19: 637\n"
56            " 41: 36692 longest_match:36863\n"
57            " 44: 36692\n"
58            " 44.2: 5861\n"
59            " 46: 13942\n"
60            " 46.1: 14003\n"
61        }
62        actual = analysis.text_to_json(test_data)
63        self.assertEqual(actual, expected)
64        test_data.close()
65
66    def test_text_to_json_empty_afdo(self):
67        expected = {}
68        actual = analysis.text_to_json("")
69        self.assertEqual(actual, expected)
70
71    def test_json_to_text(self):
72        example_prof = {"func_a": ":1\ndata\n", "func_b": ":2\nmore data\n"}
73        expected_text = "func_a:1\ndata\nfunc_b:2\nmore data\n"
74        self.assertEqual(analysis.json_to_text(example_prof), expected_text)
75
76    def test_bisect_profiles(self):
77
78        # mock run of external script with arbitrarily-chosen bad profile vals
79        # save_run specified and unused b/c afdo_prof_analysis.py
80        # will call with argument explicitly specified
81        # pylint: disable=unused-argument
82        class DeciderClass(object):
83            """Class for this tests's decider."""
84
85            def run(self, prof, save_run=False):
86                if "1" in prof["func_a"] or "3" in prof["func_b"]:
87                    return analysis.StatusEnum.BAD_STATUS
88                return analysis.StatusEnum.GOOD_STATUS
89
90        results = analysis.bisect_profiles_wrapper(
91            DeciderClass(), self.good_items, self.bad_items
92        )
93        self.assertEqual(results["individuals"], sorted(["func_a", "func_b"]))
94        self.assertEqual(results["ranges"], [])
95
96    def test_range_search(self):
97
98        # arbitrarily chosen functions whose values in the bad profile constitute
99        # a problematic pair
100        # pylint: disable=unused-argument
101        class DeciderClass(object):
102            """Class for this tests's decider."""
103
104            def run(self, prof, save_run=False):
105                if "1" in prof["func_a"] and "3" in prof["func_b"]:
106                    return analysis.StatusEnum.BAD_STATUS
107                return analysis.StatusEnum.GOOD_STATUS
108
109        # put the problematic combination in separate halves of the common funcs
110        # so that non-bisecting search is invoked for its actual use case
111        common_funcs = [
112            func for func in self.good_items if func in self.bad_items
113        ]
114        common_funcs.remove("func_a")
115        common_funcs.insert(0, "func_a")
116        common_funcs.remove("func_b")
117        common_funcs.append("func_b")
118
119        problem_range = analysis.range_search(
120            DeciderClass(),
121            self.good_items,
122            self.bad_items,
123            common_funcs,
124            0,
125            len(common_funcs),
126        )
127
128        self.assertEqual(["func_a", "func_b"], problem_range)
129
130    def test_check_good_not_bad(self):
131        func_in_good = "func_c"
132
133        # pylint: disable=unused-argument
134        class DeciderClass(object):
135            """Class for this tests's decider."""
136
137            def run(self, prof, save_run=False):
138                if func_in_good in prof:
139                    return analysis.StatusEnum.GOOD_STATUS
140                return analysis.StatusEnum.BAD_STATUS
141
142        self.assertTrue(
143            analysis.check_good_not_bad(
144                DeciderClass(), self.good_items, self.bad_items
145            )
146        )
147
148    def test_check_bad_not_good(self):
149        func_in_bad = "func_d"
150
151        # pylint: disable=unused-argument
152        class DeciderClass(object):
153            """Class for this tests's decider."""
154
155            def run(self, prof, save_run=False):
156                if func_in_bad in prof:
157                    return analysis.StatusEnum.BAD_STATUS
158                return analysis.StatusEnum.GOOD_STATUS
159
160        self.assertTrue(
161            analysis.check_bad_not_good(
162                DeciderClass(), self.good_items, self.bad_items
163            )
164        )
165
166
167if __name__ == "__main__":
168    unittest.main()
169