xref: /aosp_15_r20/external/toolchain-utils/bestflags/task.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
1# Copyright 2013 The ChromiumOS Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4"""A reproducing entity.
5
6Part of the Chrome build flags optimization.
7
8The Task class is used by different modules. Each module fills in the
9corresponding information into a Task instance. Class Task contains the bit set
10representing the flags selection. The builder module is responsible for filling
11the image and the checksum field of a Task. The executor module will put the
12execution output to the execution field.
13"""
14
15__author__ = "[email protected] (Yuheng Long)"
16
17import os
18import subprocess
19import sys
20from uuid import uuid4
21
22
23BUILD_STAGE = 1
24TEST_STAGE = 2
25
26# Message indicating that the build or test failed.
27ERROR_STRING = "error"
28
29# The maximum number of tries a build can have. Some compilations may fail due
30# to unexpected environment circumstance. This variable defines how many tries
31# the build should attempt before giving up.
32BUILD_TRIES = 3
33
34# The maximum number of tries a test can have. Some tests may fail due to
35# unexpected environment circumstance. This variable defines how many tries the
36# test should attempt before giving up.
37TEST_TRIES = 3
38
39
40# Create the file/directory if it does not already exist.
41def _CreateDirectory(file_name):
42    directory = os.path.dirname(file_name)
43    if not os.path.exists(directory):
44        os.makedirs(directory)
45
46
47class Task(object):
48    """A single reproducing entity.
49
50    A single test of performance with a particular set of flags. It records the
51    flag set, the image, the check sum of the image and the cost.
52    """
53
54    # The command that will be used in the build stage to compile the tasks.
55    BUILD_COMMAND = None
56    # The command that will be used in the test stage to test the tasks.
57    TEST_COMMAND = None
58    # The directory to log the compilation and test results.
59    LOG_DIRECTORY = None
60
61    @staticmethod
62    def InitLogCommand(build_command, test_command, log_directory):
63        """Set up the build and test command for the task and the log directory.
64
65        This framework is generic. It lets the client specify application specific
66        compile and test methods by passing different build_command and
67        test_command.
68
69        Args:
70          build_command: The command that will be used in the build stage to compile
71            this task.
72          test_command: The command that will be used in the test stage to test this
73            task.
74          log_directory: The directory to log the compilation and test results.
75        """
76
77        Task.BUILD_COMMAND = build_command
78        Task.TEST_COMMAND = test_command
79        Task.LOG_DIRECTORY = log_directory
80
81    def __init__(self, flag_set):
82        """Set up the optimization flag selection for this task.
83
84        Args:
85          flag_set: The optimization flag set that is encapsulated by this task.
86        """
87
88        self._flag_set = flag_set
89
90        # A unique identifier that distinguishes this task from other tasks.
91        self._task_identifier = uuid4()
92
93        self._log_path = (Task.LOG_DIRECTORY, self._task_identifier)
94
95        # Initiate the hash value. The hash value is used so as not to recompute it
96        # every time the hash method is called.
97        self._hash_value = None
98
99        # Indicate that the task has not been compiled/tested.
100        self._build_cost = None
101        self._exe_cost = None
102        self._checksum = None
103        self._image = None
104        self._file_length = None
105        self._text_length = None
106
107    def __eq__(self, other):
108        """Test whether two tasks are equal.
109
110        Two tasks are equal if their flag_set are equal.
111
112        Args:
113          other: The other task with which this task is tested equality.
114        Returns:
115          True if the encapsulated flag sets are equal.
116        """
117        if isinstance(other, Task):
118            return self.GetFlags() == other.GetFlags()
119        return False
120
121    def __hash__(self):
122        if self._hash_value is None:
123            # Cache the hash value of the flags, so as not to recompute them.
124            self._hash_value = hash(self._flag_set)
125        return self._hash_value
126
127    def GetIdentifier(self, stage):
128        """Get the identifier of the task in the stage.
129
130        The flag set uniquely identifies a task in the build stage. The checksum of
131        the image of the task uniquely identifies the task in the test stage.
132
133        Args:
134          stage: The stage (build/test) in which this method is called.
135        Returns:
136          Return the flag set in build stage and return the checksum in test stage.
137        """
138
139        # Define the dictionary for different stage function lookup.
140        get_identifier_functions = {
141            BUILD_STAGE: self.FormattedFlags,
142            TEST_STAGE: self.__GetCheckSum,
143        }
144
145        assert stage in get_identifier_functions
146        return get_identifier_functions[stage]()
147
148    def GetResult(self, stage):
149        """Get the performance results of the task in the stage.
150
151        Args:
152          stage: The stage (build/test) in which this method is called.
153        Returns:
154          Performance results.
155        """
156
157        # Define the dictionary for different stage function lookup.
158        get_result_functions = {
159            BUILD_STAGE: self.__GetBuildResult,
160            TEST_STAGE: self.GetTestResult,
161        }
162
163        assert stage in get_result_functions
164
165        return get_result_functions[stage]()
166
167    def SetResult(self, stage, result):
168        """Set the performance results of the task in the stage.
169
170        This method is called by the pipeling_worker to set the results for
171        duplicated tasks.
172
173        Args:
174          stage: The stage (build/test) in which this method is called.
175          result: The performance results of the stage.
176        """
177
178        # Define the dictionary for different stage function lookup.
179        set_result_functions = {
180            BUILD_STAGE: self.__SetBuildResult,
181            TEST_STAGE: self.__SetTestResult,
182        }
183
184        assert stage in set_result_functions
185
186        set_result_functions[stage](result)
187
188    def Done(self, stage):
189        """Check whether the stage is done.
190
191        Args:
192          stage: The stage to be checked, build or test.
193        Returns:
194          True if the stage is done.
195        """
196
197        # Define the dictionary for different result string lookup.
198        done_string = {
199            BUILD_STAGE: self._build_cost,
200            TEST_STAGE: self._exe_cost,
201        }
202
203        assert stage in done_string
204
205        return done_string[stage] is not None
206
207    def Work(self, stage):
208        """Perform the task.
209
210        Args:
211          stage: The stage in which the task is performed, compile or test.
212        """
213
214        # Define the dictionary for different stage function lookup.
215        work_functions = {BUILD_STAGE: self.__Compile, TEST_STAGE: self.__Test}
216
217        assert stage in work_functions
218
219        work_functions[stage]()
220
221    def FormattedFlags(self):
222        """Format the optimization flag set of this task.
223
224        Returns:
225          The formatted optimization flag set that is encapsulated by this task.
226        """
227        return str(self._flag_set.FormattedForUse())
228
229    def GetFlags(self):
230        """Get the optimization flag set of this task.
231
232        Returns:
233          The optimization flag set that is encapsulated by this task.
234        """
235
236        return self._flag_set
237
238    def __GetCheckSum(self):
239        """Get the compilation image checksum of this task.
240
241        Returns:
242          The compilation image checksum of this task.
243        """
244
245        # The checksum should be computed before this method is called.
246        assert self._checksum is not None
247        return self._checksum
248
249    def __Compile(self):
250        """Run a compile.
251
252        This method compile an image using the present flags, get the image,
253        test the existent of the image and gathers monitoring information, and sets
254        the internal cost (fitness) for this set of flags.
255        """
256
257        # Format the flags as a string as input to compile command. The unique
258        # identifier is passed to the compile command. If concurrent processes are
259        # used to compile different tasks, these processes can use the identifier to
260        # write to different file.
261        flags = self._flag_set.FormattedForUse()
262        command = "%s %s %s" % (
263            Task.BUILD_COMMAND,
264            " ".join(flags),
265            self._task_identifier,
266        )
267
268        # Try BUILD_TRIES number of times before confirming that the build fails.
269        for _ in range(BUILD_TRIES):
270            try:
271                # Execute the command and get the execution status/results.
272                p = subprocess.Popen(
273                    command.split(),
274                    stdout=subprocess.PIPE,
275                    stderr=subprocess.PIPE,
276                )
277                (out, err) = p.communicate()
278
279                if out:
280                    out = out.strip()
281                    if out != ERROR_STRING:
282                        # Each build results contains the checksum of the result image, the
283                        # performance cost of the build, the compilation image, the length
284                        # of the build, and the length of the text section of the build.
285                        (
286                            checksum,
287                            cost,
288                            image,
289                            file_length,
290                            text_length,
291                        ) = out.split()
292                        # Build successfully.
293                        break
294
295                # Build failed.
296                cost = ERROR_STRING
297            except _:
298                # If there is exception getting the cost information of the build, the
299                # build failed.
300                cost = ERROR_STRING
301
302        # Convert the build cost from String to integer. The build cost is used to
303        # compare a task with another task. Set the build cost of the failing task
304        # to the max integer. The for loop will keep trying until either there is a
305        # success or BUILD_TRIES number of tries have been conducted.
306        self._build_cost = sys.maxint if cost == ERROR_STRING else float(cost)
307
308        self._checksum = checksum
309        self._file_length = file_length
310        self._text_length = text_length
311        self._image = image
312
313        self.__LogBuildCost(err)
314
315    def __Test(self):
316        """__Test the task against benchmark(s) using the input test command."""
317
318        # Ensure that the task is compiled before being tested.
319        assert self._image is not None
320
321        # If the task does not compile, no need to test.
322        if self._image == ERROR_STRING:
323            self._exe_cost = ERROR_STRING
324            return
325
326        # The unique identifier is passed to the test command. If concurrent
327        # processes are used to compile different tasks, these processes can use the
328        # identifier to write to different file.
329        command = "%s %s %s" % (
330            Task.TEST_COMMAND,
331            self._image,
332            self._task_identifier,
333        )
334
335        # Try TEST_TRIES number of times before confirming that the build fails.
336        for _ in range(TEST_TRIES):
337            try:
338                p = subprocess.Popen(
339                    command.split(),
340                    stdout=subprocess.PIPE,
341                    stderr=subprocess.PIPE,
342                )
343                (out, err) = p.communicate()
344
345                if out:
346                    out = out.strip()
347                    if out != ERROR_STRING:
348                        # The test results contains the performance cost of the test.
349                        cost = out
350                        # Test successfully.
351                        break
352
353                # Test failed.
354                cost = ERROR_STRING
355            except _:
356                # If there is exception getting the cost information of the test, the
357                # test failed. The for loop will keep trying until either there is a
358                # success or TEST_TRIES number of tries have been conducted.
359                cost = ERROR_STRING
360
361        self._exe_cost = sys.maxint if (cost == ERROR_STRING) else float(cost)
362
363        self.__LogTestCost(err)
364
365    def __SetBuildResult(
366        self, (checksum, build_cost, image, file_length, text_length)
367    ):
368        self._checksum = checksum
369        self._build_cost = build_cost
370        self._image = image
371        self._file_length = file_length
372        self._text_length = text_length
373
374    def __GetBuildResult(self):
375        return (
376            self._checksum,
377            self._build_cost,
378            self._image,
379            self._file_length,
380            self._text_length,
381        )
382
383    def GetTestResult(self):
384        return self._exe_cost
385
386    def __SetTestResult(self, exe_cost):
387        self._exe_cost = exe_cost
388
389    def LogSteeringCost(self):
390        """Log the performance results for the task.
391
392        This method is called by the steering stage and this method writes the
393        results out to a file. The results include the build and the test results.
394        """
395
396        steering_log = "%s/%s/steering.txt" % self._log_path
397
398        _CreateDirectory(steering_log)
399
400        with open(steering_log, "w") as out_file:
401            # Include the build and the test results.
402            steering_result = (
403                self._flag_set,
404                self._checksum,
405                self._build_cost,
406                self._image,
407                self._file_length,
408                self._text_length,
409                self._exe_cost,
410            )
411
412            # Write out the result in the comma-separated format (CSV).
413            out_file.write("%s,%s,%s,%s,%s,%s,%s\n" % steering_result)
414
415    def __LogBuildCost(self, log):
416        """Log the build results for the task.
417
418        The build results include the compilation time of the build, the result
419        image, the checksum, the file length and the text length of the image.
420        The file length of the image includes the length of the file of the image.
421        The text length only includes the length of the text section of the image.
422
423        Args:
424          log: The build log of this task.
425        """
426
427        build_result_log = "%s/%s/build.txt" % self._log_path
428
429        _CreateDirectory(build_result_log)
430
431        with open(build_result_log, "w") as out_file:
432            build_result = (
433                self._flag_set,
434                self._build_cost,
435                self._image,
436                self._checksum,
437                self._file_length,
438                self._text_length,
439            )
440
441            # Write out the result in the comma-separated format (CSV).
442            out_file.write("%s,%s,%s,%s,%s,%s\n" % build_result)
443
444        # The build information about running the build.
445        build_run_log = "%s/%s/build_log.txt" % self._log_path
446        _CreateDirectory(build_run_log)
447
448        with open(build_run_log, "w") as out_log_file:
449            # Write out the execution information.
450            out_log_file.write("%s" % log)
451
452    def __LogTestCost(self, log):
453        """Log the test results for the task.
454
455        The test results include the runtime execution time of the test.
456
457        Args:
458          log: The test log of this task.
459        """
460
461        test_log = "%s/%s/test.txt" % self._log_path
462
463        _CreateDirectory(test_log)
464
465        with open(test_log, "w") as out_file:
466            test_result = (self._flag_set, self._checksum, self._exe_cost)
467
468            # Write out the result in the comma-separated format (CSV).
469            out_file.write("%s,%s,%s\n" % test_result)
470
471        # The execution information about running the test.
472        test_run_log = "%s/%s/test_log.txt" % self._log_path
473
474        _CreateDirectory(test_run_log)
475
476        with open(test_run_log, "w") as out_log_file:
477            # Append the test log information.
478            out_log_file.write("%s" % log)
479
480    def IsImproved(self, other):
481        """Compare the current task with another task.
482
483        Args:
484          other: The other task against which the current task is compared.
485
486        Returns:
487          True if this task has improvement upon the other task.
488        """
489
490        # The execution costs must have been initiated.
491        assert self._exe_cost is not None
492        assert other.GetTestResult() is not None
493
494        return self._exe_cost < other.GetTestResult()
495