| # Copyright 2019 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import os |
| import re |
| import sys |
| from typing import Dict, FrozenSet, List, Match, Optional, Tuple, Union |
| import unittest.mock as mock |
| |
| from telemetry.internal.platform import gpu_info as tgi |
| |
| # This set must be the union of the driver tags used in WebGL and WebGL2 |
| # expectations files. |
| # Examples: |
| # intel_lt_25.20.100.6577 |
| # mesa_ge_20.1 |
| EXPECTATIONS_DRIVER_TAGS = frozenset([ |
| 'mesa_lt_19.1', |
| 'mesa_ge_21.0', |
| ]) |
| |
| # Driver tag format: VENDOR_OPERATION_VERSION |
| DRIVER_TAG_MATCHER = re.compile( |
| r'^([a-z\d]+)_(eq|ne|ge|gt|le|lt)_([a-z\d\.]+)$') |
| |
| REMOTE_BROWSER_TYPES = [ |
| 'android-chromium', |
| 'android-webview-instrumentation', |
| 'cros-chrome', |
| 'fuchsia-chrome', |
| 'web-engine-shell', |
| 'cast-streaming-shell', |
| ] |
| |
| TAG_REPLACEMENTS = { |
| # nvidia on desktop, nvidia-coproration on Android. |
| 'nvidia-corporation': 'nvidia', |
| } |
| |
| |
| def _ParseANGLEGpuVendorString(device_string: str) -> Optional[str]: |
| if not device_string: |
| return None |
| # ANGLE's device (renderer) string is of the form: |
| # "ANGLE (vendor_string, renderer_string, gl_version profile)" |
| # This function will be used to get the first value in the tuple |
| match = re.search(r'ANGLE \((.*), .*, .*\)', device_string) |
| if match: |
| return match.group(1) |
| return None |
| |
| |
| def _GetANGLEGpuDeviceId(device_string: str) -> Optional[str]: |
| if not device_string: |
| return None |
| # ANGLE's device (renderer) string is of the form: |
| # "ANGLE (vendor_string, renderer_string, gl_version profile)" |
| # This function will be used to get the second value in the tuple |
| match = re.search(r'ANGLE \(.*, (.*), .*\)', device_string) |
| if match: |
| return match.group(1) |
| return None |
| |
| |
| def GetGpuVendorString(gpu_info: tgi.GPUInfo, index: int) -> str: |
| if gpu_info: |
| primary_gpu = gpu_info.devices[index] |
| if primary_gpu: |
| vendor_string = primary_gpu.vendor_string |
| angle_vendor_string = _ParseANGLEGpuVendorString( |
| primary_gpu.device_string) |
| vendor_id = primary_gpu.vendor_id |
| if vendor_id == 0x10DE: |
| return 'nvidia' |
| if vendor_id == 0x1002: |
| return 'amd' |
| if vendor_id == 0x8086: |
| return 'intel' |
| if angle_vendor_string: |
| return angle_vendor_string.lower() |
| if vendor_string: |
| return vendor_string.split(' ')[0].lower() |
| return 'unknown_gpu' |
| |
| |
| def GetGpuDeviceId(gpu_info: tgi.GPUInfo, index: int) -> Union[int, str]: |
| if gpu_info: |
| primary_gpu = gpu_info.devices[index] |
| if primary_gpu: |
| return (primary_gpu.device_id |
| or _GetANGLEGpuDeviceId(primary_gpu.device_string) |
| or primary_gpu.device_string) |
| return 0 |
| |
| |
| def GetGpuDriverVendor(gpu_info: tgi.GPUInfo) -> Optional[str]: |
| if gpu_info: |
| primary_gpu = gpu_info.devices[0] |
| if primary_gpu: |
| return primary_gpu.driver_vendor |
| return None |
| |
| |
| def GetGpuDriverVersion(gpu_info: tgi.GPUInfo) -> Optional[str]: |
| if gpu_info: |
| primary_gpu = gpu_info.devices[0] |
| if primary_gpu: |
| return primary_gpu.driver_version |
| return None |
| |
| |
| def GetANGLERenderer(gpu_info: tgi.GPUInfo) -> str: |
| retval = 'angle-disabled' |
| if gpu_info and gpu_info.aux_attributes: |
| gl_renderer = gpu_info.aux_attributes.get('gl_renderer') |
| if gl_renderer and 'ANGLE' in gl_renderer: |
| if 'Direct3D11' in gl_renderer: |
| retval = 'angle-d3d11' |
| elif 'Direct3D9' in gl_renderer: |
| retval = 'angle-d3d9' |
| elif 'OpenGL ES' in gl_renderer: |
| retval = 'angle-opengles' |
| elif 'OpenGL' in gl_renderer: |
| retval = 'angle-opengl' |
| elif 'Metal' in gl_renderer: |
| retval = 'angle-metal' |
| # SwiftShader first because it also contains Vulkan |
| elif 'SwiftShader' in gl_renderer: |
| retval = 'angle-swiftshader' |
| elif 'Vulkan' in gl_renderer: |
| retval = 'angle-vulkan' |
| return retval |
| |
| |
| def GetCommandDecoder(gpu_info: tgi.GPUInfo) -> str: |
| if gpu_info and gpu_info.aux_attributes and \ |
| gpu_info.aux_attributes.get('passthrough_cmd_decoder', False): |
| return 'passthrough' |
| return 'no_passthrough' |
| |
| |
| def GetSkiaRenderer(gpu_feature_status: Dict[str, str], |
| extra_browser_args: List[str]) -> str: |
| retval = 'renderer-software' |
| skia_renderer_enabled = ( |
| gpu_feature_status |
| and gpu_feature_status.get('gpu_compositing') == 'enabled') |
| if skia_renderer_enabled: |
| if HasDawnSkiaRenderer(extra_browser_args): |
| retval = 'renderer-skia-dawn' |
| elif HasVulkanSkiaRenderer(gpu_feature_status): |
| retval = 'renderer-skia-vulkan' |
| # The check for GL must come after Vulkan since the 'opengl' feature can be |
| # enabled for WebGL and interop even if SkiaRenderer is using Vulkan. |
| elif HasGlSkiaRenderer(gpu_feature_status): |
| retval = 'renderer-skia-gl' |
| return retval |
| |
| |
| def GetDisplayServer(browser_type: str) -> Optional[str]: |
| # Browser types run on a remote device aren't Linux, but the host running |
| # this code uses Linux, so return early to avoid erroneously reporting a |
| # display server. |
| if browser_type in REMOTE_BROWSER_TYPES: |
| return None |
| if sys.platform.startswith('linux'): |
| if 'WAYLAND_DISPLAY' in os.environ: |
| return 'display-server-wayland' |
| return 'display-server-x' |
| return None |
| |
| |
| def GetOOPCanvasStatus(gpu_feature_status: Dict[str, str]) -> str: |
| if gpu_feature_status and gpu_feature_status.get( |
| 'canvas_oop_rasterization') == 'enabled_on': |
| return 'oop-c' |
| return 'no-oop-c' |
| |
| |
| def GetAsanStatus(gpu_info: tgi.GPUInfo) -> str: |
| if gpu_info.aux_attributes.get('is_asan', False): |
| return 'asan' |
| return 'no-asan' |
| |
| |
| def GetTargetCpuStatus(gpu_info): |
| return 'target-cpu-%s' % (gpu_info.aux_attributes.get('target_cpu_bits', |
| 'unknown'), ) |
| |
| # TODO(rivr): Use GPU feature status for Dawn instead of command line. |
| def HasDawnSkiaRenderer(extra_browser_args: List[str]) -> bool: |
| if extra_browser_args: |
| for arg in extra_browser_args: |
| if arg.startswith('--enable-features') and 'SkiaDawn' in arg: |
| return True |
| return False |
| |
| |
| def HasGlSkiaRenderer(gpu_feature_status: Dict[str, str]) -> bool: |
| return (bool(gpu_feature_status) |
| and gpu_feature_status.get('opengl') == 'enabled_on') |
| |
| |
| def HasVulkanSkiaRenderer(gpu_feature_status: Dict[str, str]) -> bool: |
| return (bool(gpu_feature_status) |
| and gpu_feature_status.get('vulkan') == 'enabled_on') |
| |
| |
| def ReplaceTags(tags: List[str]) -> List[str]: |
| """Replaces certain strings in tags to make them consistent across platforms. |
| |
| Args: |
| tags: A list of strings containing expectation tags. |
| |
| Returns: |
| |tags| but potentially with some substrings replaced. |
| """ |
| replaced_tags = [] |
| for t in tags: |
| for original, replacement in TAG_REPLACEMENTS.items(): |
| replaced_tags.append(t.replace(original, replacement)) |
| return replaced_tags |
| |
| |
| # used by unittests to create a mock arguments object |
| def GetMockArgs(webgl_version: str = '1.0.0') -> mock.MagicMock: |
| args = mock.MagicMock() |
| args.webgl_conformance_version = webgl_version |
| args.webgl2_only = False |
| # for power_measurement_integration_test.py, .url has to be None to |
| # generate the correct test lists for bots. |
| args.url = None |
| args.duration = 10 |
| args.delay = 10 |
| args.resolution = 100 |
| args.fullscreen = False |
| args.underlay = False |
| args.logdir = '/tmp' |
| args.repeat = 1 |
| args.outliers = 0 |
| args.bypass_ipg = False |
| args.expected_vendor_id = 0 |
| args.expected_device_id = 0 |
| args.browser_options = [] |
| return args |
| |
| |
| def MatchDriverTag(tag: str) -> Match[str]: |
| return DRIVER_TAG_MATCHER.match(tag.lower()) |
| |
| |
| # No good way to reduce the number of local variables, particularly since each |
| # argument is also considered a local. Also no good way to reduce the number of |
| # branches without harming readability. |
| # pylint: disable=too-many-locals,too-many-branches |
| def EvaluateVersionComparison(version: str, |
| operation: str, |
| ref_version: str, |
| os_name: Optional[str] = None, |
| driver_vendor: Optional[str] = None) -> bool: |
| def parse_version(ver: str) -> Union[Tuple[int, str], Tuple[None, None]]: |
| if ver.isdigit(): |
| return int(ver), '' |
| for i, digit in enumerate(ver): |
| if not digit.isdigit(): |
| return int(ver[:i]) if i > 0 else 0, ver[i:] |
| return None, None |
| |
| def versions_can_be_compared(ver_list1, ver_list2): |
| # If either of the two versions doesn't match the Intel driver version |
| # schema, they should not be compared. |
| if len(ver_list1) != 4 or len(ver_list2) != 4: |
| return False |
| return True |
| |
| ver_list1 = version.split('.') |
| ver_list2 = ref_version.split('.') |
| # On Windows, if the driver vendor is Intel, the driver version should be |
| # compared based on the Intel graphics driver version schema. |
| # https://www.intel.com/content/www/us/en/support/articles/000005654/graphics-drivers.html |
| if os_name == 'win' and driver_vendor == 'intel': |
| if not versions_can_be_compared(ver_list1, ver_list2): |
| return operation == 'ne' |
| |
| ver_list1 = ver_list1[2:] |
| ver_list2 = ver_list2[2:] |
| |
| for i in range(0, max(len(ver_list1), len(ver_list2))): |
| ver1 = ver_list1[i] if i < len(ver_list1) else '0' |
| ver2 = ver_list2[i] if i < len(ver_list2) else '0' |
| num1, suffix1 = parse_version(ver1) |
| num2, suffix2 = parse_version(ver2) |
| |
| if num1 is None: |
| continue |
| |
| # This comes from EXPECTATIONS_DRIVER_TAGS, so we should never fail to |
| # parse a version. |
| assert num2 is not None |
| |
| if not num1 == num2: |
| diff = num1 - num2 |
| elif suffix1 == suffix2: |
| continue |
| elif suffix1 > suffix2: |
| diff = 1 |
| else: |
| diff = -1 |
| |
| if operation == 'eq': |
| return False |
| if operation == 'ne': |
| return True |
| if operation in ('ge', 'gt'): |
| return diff > 0 |
| if operation in ('le', 'lt'): |
| return diff < 0 |
| raise Exception('Invalid operation: ' + operation) |
| |
| return operation in ('eq', 'ge', 'le') |
| # pylint: enable=too-many-locals,too-many-branches |
| |
| |
| def ExpectationsDriverTags() -> FrozenSet[str]: |
| return EXPECTATIONS_DRIVER_TAGS |