Beispiel #1
0
    def __init__(self,
                 step_names,
                 minimum_count=5,
                 combine_results=True,
                 **kwargs):
        """Initializes the PerfCountNotifier on tests starting with test_name.

    Args:
      step_names: List of perf steps names. This is needed to know perf steps
          from other steps especially when the step is successful.
      minimum_count: The number of minimum consecutive (REGRESS|IMPROVE) needed
          to notify.
      combine_results: Combine summary results email for all builders in one.
    """
        # Set defaults.
        ChromiumNotifier.__init__(self, **kwargs)

        self.minimum_count = minimum_count
        self.combine_results = combine_results
        self.step_names = step_names
        self.recent_results = None
        self.error_email = False
        self.new_email_results = {}
        self.recent_results = FailuresHistory(expiration_time=_EXPIRATION_TIME,
                                              size_limit=1000)
 def test_trivial(self):
   # Put some failures and make sure they're there.
   # No failures are expected to expire or dropped due to size limit.
   h = FailuresHistory(expiration_time=3600, size_limit=1024)
   self.assertEqual(h.GetCount(42), 0)
   h.Put(42)
   self.assertEqual(h.GetCount(42), 1)
   h.Put(42)
   self.assertEqual(h.GetCount(42), 2)
   h.Put(13)
   self.assertEqual(h.GetCount(13), 1)
   self.assertEqual(h.GetCount(42), 2)
   self.assertEqual(h.GetCount(77), 0)
Beispiel #3
0
    def __init__(self, step_names, minimum_count=5, **kwargs):
        """Initializes the PerfCountNotifier on tests starting with test_name.

    Args:
      step_names: List of perf steps names. This is needed to know perf steps
          from other steps especially when the step is successful.
      minimum_count: The number of minimum consecutive (REGRESS|IMPROVE) needed
          to notify.
    """
        # Set defaults.
        ChromiumNotifier.__init__(self, **kwargs)

        self.minimum_count = minimum_count
        self.step_names = step_names
        self.recent_results = None
        self._InitRecentResults()
        self.notifications = FailuresHistory(expiration_time=_EXPIRATION_TIME,
                                             size_limit=1000)
  def test_stress_maxsize(self):
    TEST_SIZE = 137
    h = FailuresHistory(expiration_time=3600, size_limit=TEST_SIZE)
    # Many different failures
    for f in xrange(10000):
      h.Put(f)
      # Make sure we always know about the newly added failure.
      self.assertTrue(h.GetCount(f) == 1)

    # Many failures with the same ID -> should drop most of the other failures.
    for _ in xrange(TEST_SIZE * 2):
      h.Put(42)
    self.assertTrue(h.GetCount(42) > 0)

    for _ in xrange(10000):
      h.Put(42)
      # Make sure there's always enough of repeating failures.
      self.assertTrue(h.GetCount(42) >= TEST_SIZE/2)
  def test_timer(self):
    # Test that the expiration_time works as expected.
    # The size of the history is large enough to avoid dropping.
    current_time = 0
    time.time = lambda: current_time

    h = FailuresHistory(expiration_time=5, size_limit=1024)
    for _ in xrange(100):
      h.Put(42)
      current_time += 1
    self.assertEqual(h.GetCount(42), 4)
    for _ in xrange(16):
      h.Put(42)
    self.assertEqual(h.GetCount(42), 20)
    current_time += 10
    self.assertEqual(h.GetCount(42), 0)
Beispiel #6
0
class PerfCountNotifier(ChromiumNotifier):
    """This is a status notifier that only alerts on consecutive perf changes.

  The notifier only notifies when a number of consecutive REGRESS or IMPROVE
  perf results are recorded.

  See builder.interfaces.IStatusReceiver for more information about
  parameters type.
  """
    def __init__(self,
                 step_names,
                 minimum_count=5,
                 combine_results=True,
                 **kwargs):
        """Initializes the PerfCountNotifier on tests starting with test_name.

    Args:
      step_names: List of perf steps names. This is needed to know perf steps
          from other steps especially when the step is successful.
      minimum_count: The number of minimum consecutive (REGRESS|IMPROVE) needed
          to notify.
      combine_results: Combine summary results email for all builders in one.
    """
        # Set defaults.
        ChromiumNotifier.__init__(self, **kwargs)

        self.minimum_count = minimum_count
        self.combine_results = combine_results
        self.step_names = step_names
        self.recent_results = None
        self.error_email = False
        self.new_email_results = {}
        self.recent_results = FailuresHistory(expiration_time=_EXPIRATION_TIME,
                                              size_limit=1000)

    def AddNewEmailResult(self, result):
        """Stores an email result for a builder.

    Args:
      result: A tuple of the form ('REGRESS|IMPROVE', 'value_name', 'builder').
    """
        builder_name = result[2]
        build_results = self.GetEmailResults(builder_name)
        if not result[1] in build_results[result[0]]:
            build_results[result[0]].append(result[1])
        else:
            PerfLog('(%s) email result has already been stored.' %
                    ', '.join(result))

    def GetEmailResults(self, builder_name):
        """Returns the email results for a builder."""
        if not builder_name in self.new_email_results:
            self.new_email_results[builder_name] = GetNewBuilderResult()
        return self.new_email_results[builder_name]

    def _UpdateResults(self, builder_name, results):
        """Updates the results by adding/removing from the history.

    Args:
      builder_name: Builder name the results belong to.
      results: List of result tuples, each tuple is of the form
          ('REGRESS|IMPROVE', 'value_name', 'builder').
    """
        new_results_ids = [' '.join(result) for result in results]
        # Delete the old results if the new results do not have them.
        to_delete = [
            old_id for old_id in self.recent_results.failures
            if (old_id not in new_results_ids and old_id.endswith(builder_name)
                )
        ]

        for old_id in to_delete:
            self._DeleteResult(old_id)
        # Update the new results history
        for new_id in results:
            self._StoreResult(new_id)

    def _StoreResult(self, result):
        """Stores the result value and removes counter results.

    Example: if this is a REGRESS result then it is stored and its counter
    IMPROVE result, if any, is reset.

    Args:
      result: A tuple of the form ('REGRESS|IMPROVE', 'value_name', 'builder').
    """
        self.recent_results.Put(' '.join(result))
        if result[0] == REGRESS:
            counter_id = IMPROVE + ' '.join(result[1:])
        else:
            counter_id = REGRESS + ' '.join(result[1:])
        # Reset counter_id count since this breaks the consecutive count of it.
        self._DeleteResult(counter_id)

    def _DeleteResult(self, result_id):
        """Removes the history of results identified by result_id.

    Args:
      result_id: The id of the history entry (see _StoreResult() for details).
    """
        num_results = self.recent_results.GetCount(result_id)
        if num_results > 0:
            # This is a hack into FailuresHistory since it does not allow to delete
            # entries in its history unless they are expired.
            # FailuresHistory.failures_count is the total number of entries in the
            # history limitted by FailuresHistory.size_limit.
            del self.recent_results.failures[result_id]
            self.recent_results.failures_count -= num_results

    def _DeleteAllForBuild(self, builder_name):
        """Deletes all test results related to a builder."""
        to_delete = [
            result for result in self.recent_results.failures
            if result.endswith(builder_name)
        ]
        for result in to_delete:
            self._DeleteResult(result)

    def _ResetResults(self, builder_name):
        """Reset pending email results for builder."""
        builders = [builder_name]
        if self.combine_results:
            builders = self.new_email_results.keys()
        for builder_name in builders:
            self._DeleteAllForBuild(builder_name)
            self.new_email_results[builder_name] = GetNewBuilderResult()
            self.new_email_results[builder_name][EMAIL_TIME] = time.time()

    def _IsPerfStep(self, step_status):
        """Checks if the step name is one of the defined perf tests names."""
        return self.getName(step_status) in self.step_names

    def isInterestingStep(self, build_status, step_status, results):
        """Ignore the step if it is not one of the perf results steps.

    Returns:
      True: - if a REGRESS|IMPROVE happens consecutive minimum number of times.
            - if it is not a SUCCESS step and neither REGRESS|IMPROVE.
      False: - if it is a SUCCESS step.
             - if it is a notification which has already been notified.
    """
        self.error_email = False
        step_text = ' '.join(step_status.getText())
        PerfLog('Analyzing failure text: %s.' % step_text)
        if (not self._IsPerfStep(step_status)
                or not self.isInterestingBuilder(build_status.getBuilder())):
            return False

        # In case of exceptions, sometimes results output is empty.
        if not results:
            results = [FAILURE]
        builder_name = build_status.getBuilder().getName()
        self.SetBuilderGraphURL(self.getName(step_status), build_status)
        # If it is a success step, i.e. not interesting, then reset counters.
        if results[0] == SUCCESS:
            self._DeleteAllForBuild(builder_name)
            return False

        # step_text is similar to:
        # media_tests_av_perf <div class="BuildResultInfo"> PERF_REGRESS:
        # time/t (89.07%) PERF_IMPROVE: fps/video (5.40%) </div>
        #
        # regex would return tuples of the form:
        # ('REGRESS', 'time/t', 'linux-rel')
        # ('IMPROVE', 'fps/video', 'win-debug')
        #
        # It is important to put the builder name as the last element in the tuple
        # since it is used to check tests that belong to same builder.
        step_text = ' '.join(step_status.getText())
        PerfLog('Analyzing failure text: %s.' % step_text)

        perf_regress = perf_improve = ''
        perf_results = []
        if PERF_REGRESS in step_text:
            perf_regress = step_text[step_text.find(PERF_REGRESS) +
                                     len(PERF_REGRESS) +
                                     1:step_text.find(PERF_IMPROVE)]
            perf_results.extend([
                (REGRESS, test_name, builder_name)
                for test_name in re.findall(r'(\S+) (?=\(.+\))', perf_regress)
            ])

        if PERF_IMPROVE in step_text:
            # Based on log_parser/process_log.py PerformanceChangesAsText() function,
            # we assume that PERF_REGRESS (if any) appears before PERF_IMPROVE.
            perf_improve = step_text[step_text.find(PERF_IMPROVE) +
                                     len(PERF_IMPROVE) + 1:]
            perf_results.extend([
                (IMPROVE, test_name, builder_name)
                for test_name in re.findall(r'(\S+) (?=\(.+\))', perf_improve)
            ])

        # If there is no regress or improve then this could be warning or exception.
        if not perf_results:
            if not self.recent_results.GetCount(step_text):
                PerfLog(
                    'Unrecognized step status. Reporting status as interesting.'
                )
                # Force the build box to show in email
                self.error_email = True
                self.recent_results.Put(step_text)
                return True
            else:
                PerfLog('This problem has already been notified.')
                return False

        update_list = []
        for result in perf_results:
            if len(result) != 3:
                # We expect a tuple similar to ('REGRESS', 'time/t', 'linux-rel')
                continue
            result_id = ' '.join(result)
            update_list.append(result)
            PerfLog('Result: %s happened %d times in a row.' %
                    (result_id, self.recent_results.GetCount(result_id) + 1))
            if self.recent_results.GetCount(
                    result_id) >= self.minimum_count - 1:
                # This is an interesting result! We got the minimum consecutive count of
                # this result.  Store it in email results.
                PerfLog(
                    'Result: %s happened enough consecutive times to be reported.'
                    % result_id)
                self.AddNewEmailResult(result)

        self._UpdateResults(builder_name, update_list)
        # Final decision is made based on whether there are any notifications to
        # email based on this and older build results.
        return self.ShouldSendEmail(builder_name)

    def buildMessage(self, builder_name, build_status, results, step_name):
        """Send an email about this interesting step.

    Add the perf regressions/improvements that resulted in this email if any.
    """
        PerfLog('About to send an email.')
        email_subject = self.GetEmailSubject(builder_name, build_status,
                                             results, step_name)
        email_body = self.GetEmailBody(builder_name, build_status, results,
                                       step_name)
        html_content = (
            '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">'
            '<html xmlns="http://www.w3.org/1999/xhtml"><body>%s</body></html>'
            % email_body)
        defered_object = self.BuildEmailObject(email_subject, html_content,
                                               builder_name, build_status,
                                               step_name)
        self._ResetResults(builder_name)
        return defered_object

    def ShouldSendEmail(self, builder_name):
        """Returns if we should send a summary email at this moment.

    Returns:
        True if it has been at least minimum_delay_between_alert since the
        last email sent. False otherwise.
    """
        builders = [builder_name]
        if self.combine_results:
            builders = self.new_email_results.keys()
        for builder_name in builders:
            if self._ShouldSendEmail(builder_name):
                return True
        return False

    def _ShouldSendEmail(self, builder_name):
        results = self.GetEmailResults(builder_name)
        last_time_mail_sent = results[EMAIL_TIME]
        if (last_time_mail_sent and last_time_mail_sent >
                time.time() - self.minimum_delay_between_alert):
            # Rate limit tree alerts.
            PerfLog(
                'Time since last email is too short. Should not send email.')
            return False
        # Return True if there are any builder results to email about.
        return results and (results[REGRESS] or results[IMPROVE])

    def GetEmailSubject(self, builder_name, build_status, results, step_name):
        """Returns the subject of for an email based on perf results."""
        project_name = self.master_status.getTitle()
        latest_revision = build_utils.getLatestRevision(build_status)
        result = 'changes'
        builders = [builder_name]
        if self.combine_results:
            builders = self.new_email_results.keys()
        return (
            '%s %s on %s, revision %s' %
            (project_name, result, ', '.join(builders), str(latest_revision)))

    def GetEmailHeader(self, builder_name, build_status, results, step_name):
        """Returns a header message in an email.

    Used for backward compatibility with chromium_notifier.  It allows the
    users to add text to every email from the master.cfg setup.
    """
        status_text = self.status_header % {
            'builder': builder_name,
            'steps': step_name,
            'results': results
        }
        return status_text

    def GetEmailBody(self, builder_name, build_status, results, step_name):
        """Returns the main email body content."""
        email_body = ''
        builders = [builder_name]
        if self.combine_results:
            builders = self.new_email_results.keys()
        for builder_name in builders:
            email_body += '%s%s\n' % (self.GetEmailHeader(
                builder_name, build_status, results,
                step_name), self.GetPerfEmailBody(builder_name))
        # Latest build box is not relevant with multiple builder results combined.
        if not self.combine_results or self.error_email:
            email_body += (
                '\n\nLatest build results:%s' %
                self.GenStepBox(builder_name, build_status, step_name))
        PerfLog('Perf email body: %s' % email_body)
        return email_body.replace('\n', '<br>')

    def GetPerfEmailBody(self, builder_name):
        builder_results = self.GetEmailResults(builder_name)
        graph_url = builder_results[GRAPH_URL]
        msg = ''
        # Add regression HTML links.
        if builder_results[REGRESS]:
            test_urls = CreateHTMLTestURLList(graph_url,
                                              builder_results[REGRESS])
            msg += '<strong>%s</strong>: %s.\n' % (PERF_REGRESS,
                                                   ', '.join(test_urls))
        # Add improvement HTML links.
        if builder_results[IMPROVE]:
            test_urls = CreateHTMLTestURLList(graph_url,
                                              builder_results[IMPROVE])
            msg += '<strong>%s</strong>: %s.\n' % (PERF_IMPROVE,
                                                   ', '.join(test_urls))
        return msg or 'No perf results.\n'

    def BuildEmailObject(self, email_subject, html_content, builder_name,
                         build_status, step_name):
        """Creates an email object ready to be sent."""
        m = MIMEMultipart('alternative')
        m.attach(MIMEText(html_content, 'html', 'iso-8859-1'))
        m['Date'] = formatdate(localtime=True)
        m['Subject'] = email_subject
        m['From'] = self.fromaddr
        if self.reply_to:
            m['Reply-To'] = self.reply_to
        recipients = list(self.extraRecipients[:])
        dl = []
        if self.sendToInterestedUsers and self.lookup:
            for u in build_status.getInterestedUsers():
                d = defer.maybeDeferred(self.lookup.getAddress, u)
                d.addCallback(recipients.append)
                dl.append(d)
        defered_object = defer.DeferredList(dl)
        defered_object.addCallback(self._gotRecipients, recipients, m)
        defered_object.addCallback(self.getFinishedMessage, builder_name,
                                   build_status, step_name)
        return defered_object

    def GenStepBox(self, builder_name, build_status, step_name):
        """Generates a HTML styled summary box for one step."""
        waterfall_url = self.master_status.getBuildbotURL()
        styles = dict(build_utils.DEFAULT_STYLES)
        builder_results = self.GetEmailResults(builder_name)
        if builder_results[IMPROVE] and not builder_results[REGRESS]:
            styles['warnings'] = styles['success']
        return build_utils.EmailableBuildTable_bb8(build_status,
                                                   waterfall_url,
                                                   styles=styles,
                                                   step_names=[step_name])

    def SetBuilderGraphURL(self, step_name, build_status):
        """Stores the graph URL used in emails for this builder."""
        builder_name = build_status.getBuilder().getName()
        builder_results = self.GetEmailResults(builder_name)
        graph_url = GetGraphURL(step_name, build_status)
        latest_revision = build_utils.getLatestRevision(build_status)
        if latest_revision:
            graph_url = SetQueryParameter(graph_url, 'rev', latest_revision)
        builder_results[GRAPH_URL] = graph_url
 def test_maxsize(self):
   # Test that size_limit works as expected.
   # No failures should expire.
   h = FailuresHistory(expiration_time=3600, size_limit=4)
   self.assertEqual(h.GetCount(42), 0)
   h.Put(42)
   self.assertEqual(h.GetCount(42), 1)
   h.Put(42)
   self.assertEqual(h.GetCount(42), 2)
   h.Put(42)
   self.assertEqual(h.GetCount(42), 3)
   h.Put(42)
   self.assertEqual(h.GetCount(42), 4)
   h.Put(13)  # 4+1 = 5 > size_limit  ->  drop one old '42' failure.
   self.assertEqual(h.GetCount(13), 1)
   self.assertEqual(h.GetCount(42), 3)
Beispiel #8
0
 def _InitRecentResults(self):
     """Initializes a new failures history object to store results."""
     self.recent_results = FailuresHistory(expiration_time=_EXPIRATION_TIME,
                                           size_limit=1000)
Beispiel #9
0
class PerfCountNotifier(ChromiumNotifier):
    """This is a status notifier that only alerts on consecutive perf changes.

  The notifier only notifies when a number of consecutive REGRESS or IMPROVE
  perf results are recorded.

  See builder.interfaces.IStatusReceiver for more information about
  parameters type.
  """
    def __init__(self, step_names, minimum_count=5, **kwargs):
        """Initializes the PerfCountNotifier on tests starting with test_name.

    Args:
      step_names: List of perf steps names. This is needed to know perf steps
          from other steps especially when the step is successful.
      minimum_count: The number of minimum consecutive (REGRESS|IMPROVE) needed
          to notify.
    """
        # Set defaults.
        ChromiumNotifier.__init__(self, **kwargs)

        self.minimum_count = minimum_count
        self.step_names = step_names
        self.recent_results = None
        self._InitRecentResults()
        self.notifications = FailuresHistory(expiration_time=_EXPIRATION_TIME,
                                             size_limit=1000)

    def _InitRecentResults(self):
        """Initializes a new failures history object to store results."""
        self.recent_results = FailuresHistory(expiration_time=_EXPIRATION_TIME,
                                              size_limit=1000)

    def _UpdateResults(self, results):
        """Updates the results by adding/removing from the history.

    Args:
      results: List of result tuples, each tuple is of the form
          ('REGRESS|IMPROVE', 'value_name').
    """
        new_results_ids = [' '.join(result) for result in results]
        # Delete the old results if the new results do not have them.
        to_delete = [
            old_id for old_id in self.recent_results.failures
            if old_id not in new_results_ids
        ]

        for old_id in to_delete:
            self._DeleteResult(old_id)

        # Update the new results history
        for new_id in results:
            self._StoreResult(new_id)

    def _StoreResult(self, result):
        """Stores the result value and removes counter results.

    Example: if this is a REGRESS result then it is stored and its counter
    IMPROVE result, if any, is reset.

    Args:
      result: A tuple of the form ('REGRESS|IMPROVE', 'value_name').
    """
        self.recent_results.Put(' '.join(result))
        if result[0] == 'REGRESS':
            counter_id = 'IMPROVE ' + result[1]
        else:
            counter_id = 'REGRESS ' + result[1]
        # Reset counter_id count since this breaks the consecutive count of it.
        self._DeleteResult(counter_id)

    def _DeleteResult(self, result_id):
        """Removes the history of results identified by result_id.

    Args:
      result_id: The id of the history entry (see _StoreResult() for details).
    """
        num_results = self.recent_results.GetCount(result_id)
        if num_results > 0:
            # This is a hack into FailuresHistory since it does not allow to delete
            # entries in its history unless they are expired.
            # FailuresHistory.failures_count is the total number of entries in the
            # history limitted by FailuresHistory.size_limit.
            del self.recent_results.failures[result_id]
            self.recent_results.failures_count -= num_results

    def _IsPerfStep(self, step_status):
        """Checks if the step name is one of the defined perf tests names."""
        return self.getName(step_status) in self.step_names

    def isInterestingStep(self, build_status, step_status, results):
        """Ignore the step if it is not one of the perf results steps.

    Returns:
      True: - if a REGRESS|IMPROVE happens consecutive minimum number of times.
            - if it is not a SUCCESS step and neither REGRESS|IMPROVE.
      False: - if it is a SUCCESS step.
             - if it is a notification which has already been notified.
    """
        if not self._IsPerfStep(step_status):
            return False

        # In case of exceptions, sometimes results output is empty.
        if not results:
            results = [FAILURE]

        # If it is a success step, i.e. not interesting, then reset counters.
        if results[0] == SUCCESS:
            self._InitRecentResults()
            return False

        # step_text is similar to:
        # media_tests_av_perf <div class="BuildResultInfo"> PERF_REGRESS:
        # time/t (89.07%) PERF_IMPROVE: fps/video (5.40%) </div>
        #
        # regex would return tuples of the form:
        # ('REGRESS', 'time/t')
        # ('IMPROVE', 'fps/video')
        step_text = ' '.join(step_status.getText())
        log.msg('[PerfCountNotifier] Analyzing failure text: %s.' % step_text)

        perf_results = re.findall('PERF_(REGRESS|IMPROVE): (\S+)', step_text)

        # If there is no regress or improve then this could be warning or exception.
        if not perf_results:
            if not self.notifications.GetCount(step_text):
                log.msg(
                    '[PerfCountNotifier] Unrecognized step status encountered. '
                    'Reporting status as interesting.')
                self.notifications.Put(step_text)
                return True
            else:
                log.msg(
                    '[PerfCountNotifier] This problem has already been notified.'
                )
                return False

        is_interesting = False
        update_list = []
        for result in perf_results:
            if len(result) != 2:
                # We expect a tuple similar to ('REGRESS', 'time/t')
                continue
            result_id = ' '.join(result)
            update_list.append(result)
            log.msg(
                '[PerfCountNotifier] Result: %s happened %d times in a row.' %
                (result_id, self.recent_results.GetCount(result_id) + 1))
            if self.recent_results.GetCount(
                    result_id) >= self.minimum_count - 1:
                # This is an interesting result! We got the minimum consecutive count of
                # this result, however we still need to check if its been notified.
                if not self.notifications.GetCount(result_id):
                    log.msg(
                        '[PerfCountNotifier] Result: %s happened enough consecutive '
                        'times to be reported.' % result_id)
                    self.notifications.Put(result_id)
                    is_interesting = True
                else:
                    log.msg(
                        '[PerfCountNotifier] Result: %s has already been notified.'
                        % result_id)

        self._UpdateResults(update_list)

        return is_interesting