普通文本  |  147行  |  5.84 KB

# Copyright (c) 2012 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.

"""Automated performance regression detection tool for ChromeOS perf tests.

   Refer to the instruction on how to use this tool at
   https://sites.google.com/a/chromium.org/dev/perf-regression-detection.
"""

import logging
import os
import re

import common
from autotest_lib.client.common_lib import site_utils


class TraceNotFound(RuntimeError):
    """Catch the error when an expectation is not defined for a trace."""
    pass


def divide(x, y):
    if y == 0:
        return float('inf')
    return float(x) / y


class perf_expectation_checker(object):
    """Check performance results against expectations."""

    def __init__(self, test_name, board=None,
                 expectation_file_path=None):
        """Initialize a perf expectation checker.

           @param test_name: the name of the performance test,
               will be used to load the expectation.
           @param board: an alternative board name, will be used
               to load the expectation. Defaults to the board name
               in /etc/lsb-release.
           @expectation_file_path: an alternative expectation file.
               Defaults to perf_expectations.json under the same folder
               of this file.
        """
        self._expectations = {}
        if expectation_file_path:
            self._expectation_file_path = expectation_file_path
        else:
            self._expectation_file_path = os.path.abspath(
                os.path.join(os.path.dirname(__file__),
                    'perf_expectations.json'))
        self._board = board or site_utils.get_current_board()
        self._test_name = test_name
        assert self._board, 'Failed to get board name.'
        assert self._test_name, (
               'You must specify a test name when initialize'
               ' perf_expectation_checker.')
        self._load_perf_expectations_file()

    def _load_perf_expectations_file(self):
        """Load perf expectation file."""
        try:
            expectation_file = open(self._expectation_file_path)
        except IOError, e:
            logging.error('I/O Error reading expectations %s(%s): %s',
                          self._expectation_file_path, e.errno, e.strerror)
            raise e
        # Must import here to make it work with autotest.
        import json
        try:
            self._expectations = json.load(expectation_file)
        except ValueError, e:
            logging.error('ValueError parsing expectations %s(%s): %s',
                          self._expectation_file_path, e.errno, e.strerror)
            raise e
        finally:
            expectation_file.close()

        if not self._expectations:
            # Will skip checking the perf values against expectations
            # when no expecation is defined.
            logging.info('No expectation data found in %s.',
                         self._expectation_file_path)
            return

    def compare_one_trace(self, trace, trace_perf_value):
        """Compare a performance value of a trace with the expectation.

        @param trace: the name of the trace
        @param trace_perf_value: the performance value of the trace.
        @return a tuple like one of the below
            ('regress', 2.3), ('improve', 3.2), ('accept', None)
            where the float numbers are regress/improve ratios,
            or None if expectation for trace is not defined.
        """
        perf_key = '/'.join([self._board, self._test_name, trace])
        if perf_key not in self._expectations:
            raise TraceNotFound('Expectation for trace %s not defined' % trace)
        perf_data = self._expectations[perf_key]
        regress = float(perf_data['regress'])
        improve = float(perf_data['improve'])
        if (('better' in perf_data and perf_data['better'] == 'lower') or
            ('better' not in perf_data and regress > improve)):
            # The "lower is better" case.
            if trace_perf_value < improve:
                ratio = 1 - divide(trace_perf_value, improve)
                return 'improve', ratio
            elif trace_perf_value > regress:
                ratio = divide(trace_perf_value, regress) - 1
                return 'regress', ratio
        else:
            # The "higher is better" case.
            if trace_perf_value > improve:
                ratio = divide(trace_perf_value, improve) - 1
                return 'improve', ratio
            elif trace_perf_value < regress:
                ratio = 1 - divide(trace_perf_value, regress)
                return 'regress', ratio
        return 'accept', None

    def compare_multiple_traces(self, perf_results):
        """Compare multiple traces with corresponding expectations.

        @param perf_results: a dictionary from trace name to value in float,
            e.g {"milliseconds_NewTabCalendar": 1231.000000
                 "milliseconds_NewTabDocs": 889.000000}.

        @return a dictionary of regressions, improvements, and acceptances
            of the format below:
            {'regress': [('trace_1', 2.35), ('trace_2', 2.83)...],
             'improve': [('trace_3', 2.55), ('trace_3', 52.33)...],
             'accept':  ['trace_4', 'trace_5'...]}
            where the float number is the regress/improve ratio.
        """
        ret_val = {'regress':[], 'improve':[], 'accept':[]}
        for trace in perf_results:
            try:
                # (key, ratio) is like ('regress', 2.83)
                key, ratio = self.compare_one_trace(trace, perf_results[trace])
                ret_val[key].append((trace, ratio))
            except TraceNotFound:
                logging.debug(
                    'Skip checking %s/%s/%s, expectation not defined.',
                    self._board, self._test_name, trace)
        return ret_val