summaryrefslogtreecommitdiff
path: root/bin/explore-kconfig.py
blob: 7d918e9e2e9781374a47f97a4485ab72e7c035b5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
#!/usr/bin/env python3

"""explore-kconfig - Obtain build attributes of configuration variants

explore-kconfig obtains build attributes such as ROM or RAM usage of
configuration variants for a given software project. It works on random
configurations (--random) or in the neighbourhood of existing configurations
(--neighbourhood).

Supported projects must be configurable via kconfig and provide a command which
outputs a JSON dict of build attributes on stdout. Use
--{clean,build,attribute}-command to configure explore-kconfig for a project.

explore-kconfig places the experiment results (containing configurations, build
logs, and correspondnig attributes) in the current working directory. Use
analyze-kconfig to build a model once data acquisition is complete.
"""

import argparse
import logging
import os
import sys
import time

from dfatool import kconfig


def main():
    parser = argparse.ArgumentParser(
        formatter_class=argparse.RawDescriptionHelpFormatter, description=__doc__
    )
    parser.add_argument(
        "--neighbourhood",
        type=str,
        help="Explore neighbourhood of provided .config file(s)",
    )
    parser.add_argument(
        "--log-level",
        default=logging.INFO,
        type=lambda level: getattr(logging, level.upper()),
        help="Set log level",
    )
    parser.add_argument(
        "--enumerate",
        action="store_true",
        help="Enumerate all valid configurations (DEPRECATED and UNMAINTAINED)",
    )
    parser.add_argument(
        "--random",
        type=int,
        help="Explore a number of random configurations (make randconfig)",
    )
    parser.add_argument(
        "--random-int",
        action="store_true",
        help="Randomize integers after running make randconfig",
    )
    parser.add_argument(
        "--with-neighbourhood",
        action="store_true",
        help="Explore neighbourhood of successful random configurations",
    )
    parser.add_argument(
        "--repeatable",
        action="store_true",
        help="Allow repeated measurements of already benchmarked configurations",
    )
    parser.add_argument(
        "--repeat",
        type=int,
        metavar="N",
        help="Run each benchmark N times. Implies --repeatable",
    )
    parser.add_argument(
        "--clean-command", type=str, help="Clean command", default="make clean"
    )
    parser.add_argument(
        "--build-command", type=str, help="Build command", default="make"
    )
    parser.add_argument(
        "--attribute-command",
        type=str,
        help="Attribute extraction command",
        default="make nfpvalues",
    )
    parser.add_argument(
        "--randconfig-command",
        type=str,
        help="Randconfig command for --random",
        default="make randconfig",
    )
    parser.add_argument(
        "--kconfig-file", type=str, help="Kconfig file", default="Kconfig"
    )
    parser.add_argument("project_root", type=str, help="Project root directory")

    args = parser.parse_args()

    if isinstance(args.log_level, int):
        logging.basicConfig(level=args.log_level)
    else:
        print(f"Invalid log level. Setting log level to INFO.", file=sys.stderr)

    kconf = kconfig.KConfig(args.project_root)

    if args.clean_command:
        kconf.clean_command = args.clean_command
    if args.build_command:
        kconf.build_command = args.build_command
    if args.attribute_command:
        kconf.attribute_command = args.attribute_command
    if args.randconfig_command:
        kconf.randconfig_command = args.randconfig_command
    if args.kconfig_file:
        kconf.kconfig = args.kconfig_file
    if args.repeatable:
        kconf.repeatable = args.repeatable
    if args.repeat:
        kconf.repeat = args.repeat - 1
        kconf.repeatable = True

    kconf.run_nfpkeys()

    if args.enumerate:
        kconf.enumerate()

    if args.random:
        num_successful = 0
        total_randconfig_seconds = 0
        # Assumption: At least 1% of builds are successful
        for i in range(args.random * 100):
            logging.info(f"Running randconfig {num_successful+1} of {args.random}")
            if total_randconfig_seconds and num_successful:
                seconds_per_randconfig = total_randconfig_seconds / num_successful
                remaining_minutes = (
                    int(seconds_per_randconfig * (args.random - num_successful)) // 60
                )
                logging.info(
                    f"Estimated remaining exploration time: {remaining_minutes // (24*60):2d} days {(remaining_minutes % (24*60)) // 60:2d} hours {remaining_minutes % 60:2d} minutes"
                )
            randconfig_start_time = time.time()
            status = kconf.run_randconfig(with_random_int=args.random_int)
            if status["success"]:
                num_successful += 1
            if args.with_neighbourhood and status["success"]:
                config_filename = status["config_path"]
                logging.debug(f"Exploring neighbourhood of {config_filename}")
                kconf.run_exploration_from_file(
                    config_filename, with_initial_config=False
                )
            total_randconfig_seconds += time.time() - randconfig_start_time
            if num_successful == args.random:
                break

    if args.neighbourhood:
        if os.path.isfile(args.neighbourhood):
            kconf.run_exploration_from_file(args.neighbourhood)
        elif os.path.isdir(args.neighbourhood):
            for filename in os.listdir(args.neighbourhood):
                config_filename = f"{args.neighbourhood}/{filename}"
                logging.debug(f"Exploring neighbourhood of {config_filename}")
                kconf.run_exploration_from_file(config_filename)
        else:
            print(
                f"--neighbourhod: Error: {args.neighbourhood} must be a file or directory, but is neither",
                file=sys.stderr,
            )


if __name__ == "__main__":
    main()