451 lines
14 KiB
Python
451 lines
14 KiB
Python
|
# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
|
||
|
# Use of this source code is governed by a BSD-style license that can be
|
||
|
# found in the LICENSE file.
|
||
|
"""A reproducing entity.
|
||
|
|
||
|
Part of the Chrome build flags optimization.
|
||
|
|
||
|
The Task class is used by different modules. Each module fills in the
|
||
|
corresponding information into a Task instance. Class Task contains the bit set
|
||
|
representing the flags selection. The builder module is responsible for filling
|
||
|
the image and the checksum field of a Task. The executor module will put the
|
||
|
execution output to the execution field.
|
||
|
"""
|
||
|
|
||
|
__author__ = 'yuhenglong@google.com (Yuheng Long)'
|
||
|
|
||
|
import os
|
||
|
import subprocess
|
||
|
import sys
|
||
|
from uuid import uuid4
|
||
|
|
||
|
BUILD_STAGE = 1
|
||
|
TEST_STAGE = 2
|
||
|
|
||
|
# Message indicating that the build or test failed.
|
||
|
ERROR_STRING = 'error'
|
||
|
|
||
|
# The maximum number of tries a build can have. Some compilations may fail due
|
||
|
# to unexpected environment circumstance. This variable defines how many tries
|
||
|
# the build should attempt before giving up.
|
||
|
BUILD_TRIES = 3
|
||
|
|
||
|
# The maximum number of tries a test can have. Some tests may fail due to
|
||
|
# unexpected environment circumstance. This variable defines how many tries the
|
||
|
# test should attempt before giving up.
|
||
|
TEST_TRIES = 3
|
||
|
|
||
|
|
||
|
# Create the file/directory if it does not already exist.
|
||
|
def _CreateDirectory(file_name):
|
||
|
directory = os.path.dirname(file_name)
|
||
|
if not os.path.exists(directory):
|
||
|
os.makedirs(directory)
|
||
|
|
||
|
|
||
|
class Task(object):
|
||
|
"""A single reproducing entity.
|
||
|
|
||
|
A single test of performance with a particular set of flags. It records the
|
||
|
flag set, the image, the check sum of the image and the cost.
|
||
|
"""
|
||
|
|
||
|
# The command that will be used in the build stage to compile the tasks.
|
||
|
BUILD_COMMAND = None
|
||
|
# The command that will be used in the test stage to test the tasks.
|
||
|
TEST_COMMAND = None
|
||
|
# The directory to log the compilation and test results.
|
||
|
LOG_DIRECTORY = None
|
||
|
|
||
|
@staticmethod
|
||
|
def InitLogCommand(build_command, test_command, log_directory):
|
||
|
"""Set up the build and test command for the task and the log directory.
|
||
|
|
||
|
This framework is generic. It lets the client specify application specific
|
||
|
compile and test methods by passing different build_command and
|
||
|
test_command.
|
||
|
|
||
|
Args:
|
||
|
build_command: The command that will be used in the build stage to compile
|
||
|
this task.
|
||
|
test_command: The command that will be used in the test stage to test this
|
||
|
task.
|
||
|
log_directory: The directory to log the compilation and test results.
|
||
|
"""
|
||
|
|
||
|
Task.BUILD_COMMAND = build_command
|
||
|
Task.TEST_COMMAND = test_command
|
||
|
Task.LOG_DIRECTORY = log_directory
|
||
|
|
||
|
def __init__(self, flag_set):
|
||
|
"""Set up the optimization flag selection for this task.
|
||
|
|
||
|
Args:
|
||
|
flag_set: The optimization flag set that is encapsulated by this task.
|
||
|
"""
|
||
|
|
||
|
self._flag_set = flag_set
|
||
|
|
||
|
# A unique identifier that distinguishes this task from other tasks.
|
||
|
self._task_identifier = uuid4()
|
||
|
|
||
|
self._log_path = (Task.LOG_DIRECTORY, self._task_identifier)
|
||
|
|
||
|
# Initiate the hash value. The hash value is used so as not to recompute it
|
||
|
# every time the hash method is called.
|
||
|
self._hash_value = None
|
||
|
|
||
|
# Indicate that the task has not been compiled/tested.
|
||
|
self._build_cost = None
|
||
|
self._exe_cost = None
|
||
|
self._checksum = None
|
||
|
self._image = None
|
||
|
self._file_length = None
|
||
|
self._text_length = None
|
||
|
|
||
|
def __eq__(self, other):
|
||
|
"""Test whether two tasks are equal.
|
||
|
|
||
|
Two tasks are equal if their flag_set are equal.
|
||
|
|
||
|
Args:
|
||
|
other: The other task with which this task is tested equality.
|
||
|
Returns:
|
||
|
True if the encapsulated flag sets are equal.
|
||
|
"""
|
||
|
if isinstance(other, Task):
|
||
|
return self.GetFlags() == other.GetFlags()
|
||
|
return False
|
||
|
|
||
|
def __hash__(self):
|
||
|
if self._hash_value is None:
|
||
|
# Cache the hash value of the flags, so as not to recompute them.
|
||
|
self._hash_value = hash(self._flag_set)
|
||
|
return self._hash_value
|
||
|
|
||
|
def GetIdentifier(self, stage):
|
||
|
"""Get the identifier of the task in the stage.
|
||
|
|
||
|
The flag set uniquely identifies a task in the build stage. The checksum of
|
||
|
the image of the task uniquely identifies the task in the test stage.
|
||
|
|
||
|
Args:
|
||
|
stage: The stage (build/test) in which this method is called.
|
||
|
Returns:
|
||
|
Return the flag set in build stage and return the checksum in test stage.
|
||
|
"""
|
||
|
|
||
|
# Define the dictionary for different stage function lookup.
|
||
|
get_identifier_functions = {BUILD_STAGE: self.FormattedFlags,
|
||
|
TEST_STAGE: self.__GetCheckSum}
|
||
|
|
||
|
assert stage in get_identifier_functions
|
||
|
return get_identifier_functions[stage]()
|
||
|
|
||
|
def GetResult(self, stage):
|
||
|
"""Get the performance results of the task in the stage.
|
||
|
|
||
|
Args:
|
||
|
stage: The stage (build/test) in which this method is called.
|
||
|
Returns:
|
||
|
Performance results.
|
||
|
"""
|
||
|
|
||
|
# Define the dictionary for different stage function lookup.
|
||
|
get_result_functions = {BUILD_STAGE: self.__GetBuildResult,
|
||
|
TEST_STAGE: self.GetTestResult}
|
||
|
|
||
|
assert stage in get_result_functions
|
||
|
|
||
|
return get_result_functions[stage]()
|
||
|
|
||
|
def SetResult(self, stage, result):
|
||
|
"""Set the performance results of the task in the stage.
|
||
|
|
||
|
This method is called by the pipeling_worker to set the results for
|
||
|
duplicated tasks.
|
||
|
|
||
|
Args:
|
||
|
stage: The stage (build/test) in which this method is called.
|
||
|
result: The performance results of the stage.
|
||
|
"""
|
||
|
|
||
|
# Define the dictionary for different stage function lookup.
|
||
|
set_result_functions = {BUILD_STAGE: self.__SetBuildResult,
|
||
|
TEST_STAGE: self.__SetTestResult}
|
||
|
|
||
|
assert stage in set_result_functions
|
||
|
|
||
|
set_result_functions[stage](result)
|
||
|
|
||
|
def Done(self, stage):
|
||
|
"""Check whether the stage is done.
|
||
|
|
||
|
Args:
|
||
|
stage: The stage to be checked, build or test.
|
||
|
Returns:
|
||
|
True if the stage is done.
|
||
|
"""
|
||
|
|
||
|
# Define the dictionary for different result string lookup.
|
||
|
done_string = {BUILD_STAGE: self._build_cost, TEST_STAGE: self._exe_cost}
|
||
|
|
||
|
assert stage in done_string
|
||
|
|
||
|
return done_string[stage] is not None
|
||
|
|
||
|
def Work(self, stage):
|
||
|
"""Perform the task.
|
||
|
|
||
|
Args:
|
||
|
stage: The stage in which the task is performed, compile or test.
|
||
|
"""
|
||
|
|
||
|
# Define the dictionary for different stage function lookup.
|
||
|
work_functions = {BUILD_STAGE: self.__Compile, TEST_STAGE: self.__Test}
|
||
|
|
||
|
assert stage in work_functions
|
||
|
|
||
|
work_functions[stage]()
|
||
|
|
||
|
def FormattedFlags(self):
|
||
|
"""Format the optimization flag set of this task.
|
||
|
|
||
|
Returns:
|
||
|
The formatted optimization flag set that is encapsulated by this task.
|
||
|
"""
|
||
|
return str(self._flag_set.FormattedForUse())
|
||
|
|
||
|
def GetFlags(self):
|
||
|
"""Get the optimization flag set of this task.
|
||
|
|
||
|
Returns:
|
||
|
The optimization flag set that is encapsulated by this task.
|
||
|
"""
|
||
|
|
||
|
return self._flag_set
|
||
|
|
||
|
def __GetCheckSum(self):
|
||
|
"""Get the compilation image checksum of this task.
|
||
|
|
||
|
Returns:
|
||
|
The compilation image checksum of this task.
|
||
|
"""
|
||
|
|
||
|
# The checksum should be computed before this method is called.
|
||
|
assert self._checksum is not None
|
||
|
return self._checksum
|
||
|
|
||
|
def __Compile(self):
|
||
|
"""Run a compile.
|
||
|
|
||
|
This method compile an image using the present flags, get the image,
|
||
|
test the existent of the image and gathers monitoring information, and sets
|
||
|
the internal cost (fitness) for this set of flags.
|
||
|
"""
|
||
|
|
||
|
# Format the flags as a string as input to compile command. The unique
|
||
|
# identifier is passed to the compile command. If concurrent processes are
|
||
|
# used to compile different tasks, these processes can use the identifier to
|
||
|
# write to different file.
|
||
|
flags = self._flag_set.FormattedForUse()
|
||
|
command = '%s %s %s' % (Task.BUILD_COMMAND, ' '.join(flags),
|
||
|
self._task_identifier)
|
||
|
|
||
|
# Try BUILD_TRIES number of times before confirming that the build fails.
|
||
|
for _ in range(BUILD_TRIES):
|
||
|
try:
|
||
|
# Execute the command and get the execution status/results.
|
||
|
p = subprocess.Popen(command.split(),
|
||
|
stdout=subprocess.PIPE,
|
||
|
stderr=subprocess.PIPE)
|
||
|
(out, err) = p.communicate()
|
||
|
|
||
|
if out:
|
||
|
out = out.strip()
|
||
|
if out != ERROR_STRING:
|
||
|
# Each build results contains the checksum of the result image, the
|
||
|
# performance cost of the build, the compilation image, the length
|
||
|
# of the build, and the length of the text section of the build.
|
||
|
(checksum, cost, image, file_length, text_length) = out.split()
|
||
|
# Build successfully.
|
||
|
break
|
||
|
|
||
|
# Build failed.
|
||
|
cost = ERROR_STRING
|
||
|
except _:
|
||
|
# If there is exception getting the cost information of the build, the
|
||
|
# build failed.
|
||
|
cost = ERROR_STRING
|
||
|
|
||
|
# Convert the build cost from String to integer. The build cost is used to
|
||
|
# compare a task with another task. Set the build cost of the failing task
|
||
|
# to the max integer. The for loop will keep trying until either there is a
|
||
|
# success or BUILD_TRIES number of tries have been conducted.
|
||
|
self._build_cost = sys.maxint if cost == ERROR_STRING else float(cost)
|
||
|
|
||
|
self._checksum = checksum
|
||
|
self._file_length = file_length
|
||
|
self._text_length = text_length
|
||
|
self._image = image
|
||
|
|
||
|
self.__LogBuildCost(err)
|
||
|
|
||
|
def __Test(self):
|
||
|
"""__Test the task against benchmark(s) using the input test command."""
|
||
|
|
||
|
# Ensure that the task is compiled before being tested.
|
||
|
assert self._image is not None
|
||
|
|
||
|
# If the task does not compile, no need to test.
|
||
|
if self._image == ERROR_STRING:
|
||
|
self._exe_cost = ERROR_STRING
|
||
|
return
|
||
|
|
||
|
# The unique identifier is passed to the test command. If concurrent
|
||
|
# processes are used to compile different tasks, these processes can use the
|
||
|
# identifier to write to different file.
|
||
|
command = '%s %s %s' % (Task.TEST_COMMAND, self._image,
|
||
|
self._task_identifier)
|
||
|
|
||
|
# Try TEST_TRIES number of times before confirming that the build fails.
|
||
|
for _ in range(TEST_TRIES):
|
||
|
try:
|
||
|
p = subprocess.Popen(command.split(),
|
||
|
stdout=subprocess.PIPE,
|
||
|
stderr=subprocess.PIPE)
|
||
|
(out, err) = p.communicate()
|
||
|
|
||
|
if out:
|
||
|
out = out.strip()
|
||
|
if out != ERROR_STRING:
|
||
|
# The test results contains the performance cost of the test.
|
||
|
cost = out
|
||
|
# Test successfully.
|
||
|
break
|
||
|
|
||
|
# Test failed.
|
||
|
cost = ERROR_STRING
|
||
|
except _:
|
||
|
# If there is exception getting the cost information of the test, the
|
||
|
# test failed. The for loop will keep trying until either there is a
|
||
|
# success or TEST_TRIES number of tries have been conducted.
|
||
|
cost = ERROR_STRING
|
||
|
|
||
|
self._exe_cost = sys.maxint if (cost == ERROR_STRING) else float(cost)
|
||
|
|
||
|
self.__LogTestCost(err)
|
||
|
|
||
|
def __SetBuildResult(self, (checksum, build_cost, image, file_length,
|
||
|
text_length)):
|
||
|
self._checksum = checksum
|
||
|
self._build_cost = build_cost
|
||
|
self._image = image
|
||
|
self._file_length = file_length
|
||
|
self._text_length = text_length
|
||
|
|
||
|
def __GetBuildResult(self):
|
||
|
return (self._checksum, self._build_cost, self._image, self._file_length,
|
||
|
self._text_length)
|
||
|
|
||
|
def GetTestResult(self):
|
||
|
return self._exe_cost
|
||
|
|
||
|
def __SetTestResult(self, exe_cost):
|
||
|
self._exe_cost = exe_cost
|
||
|
|
||
|
def LogSteeringCost(self):
|
||
|
"""Log the performance results for the task.
|
||
|
|
||
|
This method is called by the steering stage and this method writes the
|
||
|
results out to a file. The results include the build and the test results.
|
||
|
"""
|
||
|
|
||
|
steering_log = '%s/%s/steering.txt' % self._log_path
|
||
|
|
||
|
_CreateDirectory(steering_log)
|
||
|
|
||
|
with open(steering_log, 'w') as out_file:
|
||
|
# Include the build and the test results.
|
||
|
steering_result = (self._flag_set, self._checksum, self._build_cost,
|
||
|
self._image, self._file_length, self._text_length,
|
||
|
self._exe_cost)
|
||
|
|
||
|
# Write out the result in the comma-separated format (CSV).
|
||
|
out_file.write('%s,%s,%s,%s,%s,%s,%s\n' % steering_result)
|
||
|
|
||
|
def __LogBuildCost(self, log):
|
||
|
"""Log the build results for the task.
|
||
|
|
||
|
The build results include the compilation time of the build, the result
|
||
|
image, the checksum, the file length and the text length of the image.
|
||
|
The file length of the image includes the length of the file of the image.
|
||
|
The text length only includes the length of the text section of the image.
|
||
|
|
||
|
Args:
|
||
|
log: The build log of this task.
|
||
|
"""
|
||
|
|
||
|
build_result_log = '%s/%s/build.txt' % self._log_path
|
||
|
|
||
|
_CreateDirectory(build_result_log)
|
||
|
|
||
|
with open(build_result_log, 'w') as out_file:
|
||
|
build_result = (self._flag_set, self._build_cost, self._image,
|
||
|
self._checksum, self._file_length, self._text_length)
|
||
|
|
||
|
# Write out the result in the comma-separated format (CSV).
|
||
|
out_file.write('%s,%s,%s,%s,%s,%s\n' % build_result)
|
||
|
|
||
|
# The build information about running the build.
|
||
|
build_run_log = '%s/%s/build_log.txt' % self._log_path
|
||
|
_CreateDirectory(build_run_log)
|
||
|
|
||
|
with open(build_run_log, 'w') as out_log_file:
|
||
|
# Write out the execution information.
|
||
|
out_log_file.write('%s' % log)
|
||
|
|
||
|
def __LogTestCost(self, log):
|
||
|
"""Log the test results for the task.
|
||
|
|
||
|
The test results include the runtime execution time of the test.
|
||
|
|
||
|
Args:
|
||
|
log: The test log of this task.
|
||
|
"""
|
||
|
|
||
|
test_log = '%s/%s/test.txt' % self._log_path
|
||
|
|
||
|
_CreateDirectory(test_log)
|
||
|
|
||
|
with open(test_log, 'w') as out_file:
|
||
|
test_result = (self._flag_set, self._checksum, self._exe_cost)
|
||
|
|
||
|
# Write out the result in the comma-separated format (CSV).
|
||
|
out_file.write('%s,%s,%s\n' % test_result)
|
||
|
|
||
|
# The execution information about running the test.
|
||
|
test_run_log = '%s/%s/test_log.txt' % self._log_path
|
||
|
|
||
|
_CreateDirectory(test_run_log)
|
||
|
|
||
|
with open(test_run_log, 'w') as out_log_file:
|
||
|
# Append the test log information.
|
||
|
out_log_file.write('%s' % log)
|
||
|
|
||
|
def IsImproved(self, other):
|
||
|
"""Compare the current task with another task.
|
||
|
|
||
|
Args:
|
||
|
other: The other task against which the current task is compared.
|
||
|
|
||
|
Returns:
|
||
|
True if this task has improvement upon the other task.
|
||
|
"""
|
||
|
|
||
|
# The execution costs must have been initiated.
|
||
|
assert self._exe_cost is not None
|
||
|
assert other.GetTestResult() is not None
|
||
|
|
||
|
return self._exe_cost < other.GetTestResult()
|