
We have more analyzer tools now. Update the folder name of these tools to a proper name. Bug: 1459149 Change-Id: Ib633833f44223ac794dcefd6bde06dc05c398275 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5124472 Reviewed-by: Brian Sheedy <bsheedy@chromium.org> Reviewed-by: Weizhong Xia <weizhong@google.com> Commit-Queue: Jie Sheng <jiesheng@google.com> Cr-Commit-Position: refs/heads/main@{#1238657}
214 lines
9.2 KiB
Python
214 lines
9.2 KiB
Python
# Copyright 2023 The Chromium Authors
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
"""Module for all matching analyzers."""
|
|
|
|
from typing import List
|
|
|
|
from blinkpy.web_tests.web_test_analyzers import data_types as dt
|
|
|
|
MAX_BUILDER_NUM = 5
|
|
AVG_DURATION_THRESHOLD = 0.75
|
|
|
|
|
|
class ImageMatchingAnalyzer:
|
|
"""Abstract base class for all analyzers."""
|
|
def run_analyzer(
|
|
self,
|
|
test_results: dt.TestToTypTagsType) -> dt.TestAnalysisResultType:
|
|
"""Gets the analysis result of the input test's results.
|
|
|
|
Returns:
|
|
A test analysis result representing the analyzer suggested matching
|
|
information for the input image comparison test's result.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def description(self) -> str:
|
|
"""Returns a description string representation of the analyzer."""
|
|
raise NotImplementedError()
|
|
|
|
|
|
class FuzzyMatchingAnalyzer(ImageMatchingAnalyzer):
|
|
def __init__(self,
|
|
fuzzy_match_image_diff_num_threshold: int = 3,
|
|
fuzzy_match_distinct_diff_num_threshold: int = 3):
|
|
"""Class for the fuzzy matching analyzer for web tests.
|
|
|
|
Args:
|
|
fuzzy_match_image_diff_num_threshold: An int denoting the number of
|
|
image test result at least to get the suggested fuzzy match range.
|
|
fuzzy_match_distinct_diff_num_threshold: An int denoting the number of
|
|
different image diff result at least to calculate the suggested
|
|
fuzzy match range.
|
|
"""
|
|
super().__init__()
|
|
assert fuzzy_match_image_diff_num_threshold >= 0
|
|
assert fuzzy_match_distinct_diff_num_threshold >= 0
|
|
self._image_diff_num_threshold = fuzzy_match_image_diff_num_threshold
|
|
self._distinct_diff_num_threshold = fuzzy_match_distinct_diff_num_threshold
|
|
|
|
def run_analyzer(
|
|
self,
|
|
test_results: dt.TestToTypTagsType) -> dt.TestAnalysisResultType:
|
|
"""Analyze the input image comparison test result for a fuzzy match
|
|
range suggestion.
|
|
|
|
Args:
|
|
test_results: Parsed test results of a test with image diff data.
|
|
|
|
Returns:
|
|
A test analysis result representing the analyzer suggested matching
|
|
information for the input image comparison test's result.
|
|
"""
|
|
# Check the image num threshold
|
|
total_image_diff_num = 0
|
|
image_diff_counts = {}
|
|
for image_diff_list in test_results.values():
|
|
total_image_diff_num += len(image_diff_list)
|
|
for image_diff in image_diff_list:
|
|
key = dt.ImageDiffTagTupleType(image_diff.color_difference,
|
|
image_diff.pixel_difference, "")
|
|
image_diff_counts[key] = image_diff_counts.get(key, 0) + 1
|
|
|
|
# Total image diff number does not meet the threshold, return no result.
|
|
if total_image_diff_num < self._image_diff_num_threshold:
|
|
return dt.TestAnalysisResultType(
|
|
False, 'Total image diff number is less than %d, no result' %
|
|
self._image_diff_num_threshold)
|
|
|
|
# Total distinct image diff number does not reach the threshold, return all
|
|
# these image diff as suggested matching.
|
|
if len(image_diff_counts) < self._distinct_diff_num_threshold:
|
|
result = 'Total test number that have image diff is '
|
|
result += '%d. ' % total_image_diff_num
|
|
result += 'Total distinct image diff results is less '
|
|
result += 'than %d. Suggested ' % self._distinct_diff_num_threshold
|
|
result += 'check these image diff result (color_difference, '
|
|
result += 'pixel_difference) individually to fix the issue:'
|
|
for image_diff, image_diff_number in image_diff_counts.items():
|
|
result = result + ' (%d, %d) with total test number %d' % (
|
|
image_diff.color_difference, image_diff.pixel_difference,
|
|
image_diff_number)
|
|
return dt.TestAnalysisResultType(True, result)
|
|
|
|
# Calculate the suggested fuzzy match number.
|
|
color_diff_list = []
|
|
pixel_diff_list = []
|
|
for image_diff, count in image_diff_counts.items():
|
|
color_diff_list += [image_diff.color_difference] * count
|
|
pixel_diff_list += [image_diff.pixel_difference] * count
|
|
|
|
result = 'Total test number that have image diff is '
|
|
result += '%d. ' % total_image_diff_num
|
|
result += 'The list of fuzzy match range suggested for this test: '
|
|
result += '\nFor color difference:\n'
|
|
result += self._calculate_data_percentile(color_diff_list)
|
|
result += '\nFor pixel difference:\n'
|
|
result += self._calculate_data_percentile(pixel_diff_list)
|
|
return dt.TestAnalysisResultType(True, result)
|
|
|
|
def _calculate_data_percentile(self, data_list: List[int]) -> str:
|
|
"""Calculate the input data list for the percentile result.
|
|
|
|
Args:
|
|
data_list: List of int to calculate.
|
|
|
|
Returns:
|
|
A string of percentile information.
|
|
"""
|
|
|
|
if len(data_list) > 0:
|
|
data_list.sort()
|
|
color_diff_length = len(data_list) - 1
|
|
result = '%d to cover 50 percentile, ' % data_list[int(
|
|
color_diff_length * 0.5)]
|
|
result += '%d to cover 75 ' % data_list[int(
|
|
color_diff_length * 0.75)]
|
|
result += 'percentile, %d to ' % data_list[int(
|
|
color_diff_length * 0.9)]
|
|
result += 'cover 90 percentile, %d to cover 95' % data_list[int(
|
|
color_diff_length * 0.95)]
|
|
result += ' percentile, %d to cover all.' % data_list[
|
|
color_diff_length]
|
|
return result
|
|
|
|
return "No data."
|
|
|
|
def description(self) -> str:
|
|
des = ('Fuzzy match analyzer with image_diff_num_threshold of %d and ' %
|
|
self._image_diff_num_threshold)
|
|
des += ('distinct_diff_num_threshold of %d' %
|
|
self._distinct_diff_num_threshold)
|
|
return des
|
|
|
|
|
|
class SlowTestAnalyzer:
|
|
def __init__(self,
|
|
slow_result_ratio_threshold: float = 0.9,
|
|
timeout_result_threshold: int = 0):
|
|
"""Class for the slow test analyzer for web tests.
|
|
|
|
Args: (Both thresholds must be hit for a test to be considered slow)
|
|
slow_result_ratio_threshold: A float denoting what fraction
|
|
of results need to be slow for a test to be considered slow.
|
|
timeout_result_threshold: An int denoting the number of timeout
|
|
results necessary for a test to be considered slow.
|
|
"""
|
|
assert slow_result_ratio_threshold >= 0
|
|
assert slow_result_ratio_threshold <= 1
|
|
assert timeout_result_threshold >= 0
|
|
self._slow_result_ratio_threshold = slow_result_ratio_threshold
|
|
self._timeout_result_threshold = timeout_result_threshold
|
|
|
|
def run_analyzer(
|
|
self, test_results: List[dt.TestSlownessTupleType]
|
|
) -> dt.TestAnalysisResultType:
|
|
"""Analyze the input test slowness data to show a statistics and
|
|
suggestion.
|
|
|
|
Args:
|
|
test_results: list of slowness data per builder.
|
|
|
|
Returns:
|
|
A test analysis result representing the analyzer's statistics and
|
|
suggestion for the slow test.
|
|
"""
|
|
# Filter slowness data per builder
|
|
slow_tests = []
|
|
for result in test_results:
|
|
if (result.timeout_count >= self._timeout_result_threshold
|
|
and result.avg_duration >=
|
|
result.timeout * AVG_DURATION_THRESHOLD):
|
|
total_non_timeout_count = result.slow_count + result.non_slow_count
|
|
slow_ratio = (result.slow_count / total_non_timeout_count
|
|
if total_non_timeout_count else 0)
|
|
if slow_ratio >= self._slow_result_ratio_threshold:
|
|
slow_tests.append(
|
|
dt.TestSlownessData(result.builder, result.slow_count,
|
|
slow_ratio, result.timeout_count,
|
|
result.avg_duration))
|
|
|
|
# No slowness data meet the threshold, return no result.
|
|
if not slow_tests:
|
|
return dt.TestAnalysisResultType(
|
|
False, 'Test does not meet threshold in all builders.')
|
|
|
|
sorted(slow_tests, key=lambda x: x.slow_ratio)
|
|
count = min(MAX_BUILDER_NUM, len(slow_tests))
|
|
result = 'Test is slow in the below list of builders:\n'
|
|
for i in range(count):
|
|
test = slow_tests[i]
|
|
result += '%s : timeout count: %d, ' % (test.builder,
|
|
test.timeout_count)
|
|
result += 'slow count: %d, ' % test.slow_count
|
|
result += 'slow ratio: %.2f, ' % test.slow_ratio
|
|
result += 'avg duration: %.2f\n' % test.avg_duration
|
|
return dt.TestAnalysisResultType(True, result)
|
|
|
|
def description(self) -> str:
|
|
des = (f'Slow test analyzer with slow_result_ration_threshold of '
|
|
f'{self._slow_result_ratio_threshold} and '
|
|
f'timeout_result_threshold of {self._timeout_result_threshold}')
|
|
return des
|