blob: 3dab92e0525669838ceaccb76b195056a5d64a7e [file] [log] [blame]
# 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.
"""
Provides graphics related utils, like capturing screenshots or checking on
the state of the graphics driver.
"""
import collections
import contextlib
import fcntl
import glob
import logging
import os
import re
import struct
import sys
import time
from autotest_lib.client.bin import test
from autotest_lib.client.bin import utils
from autotest_lib.client.common_lib import error
from autotest_lib.client.cros.input_playback import input_playback
from autotest_lib.client.cros.power import power_utils
from functools import wraps
# The uinput module might not be available at SDK test time.
try:
from autotest_lib.client.cros.graphics import graphics_uinput
except ImportError:
graphics_uinput = None
class GraphicsTest(test.test):
"""Base class for graphics test.
GraphicsTest is the base class for graphics tests.
Every subclass of GraphicsTest should call GraphicsTests initialize/cleanup
method as they will do GraphicsStateChecker as well as report states to
Chrome Perf dashboard.
Attributes:
_test_failure_description(str): Failure name reported to chrome perf
dashboard. (Default: Failures)
_test_failure_report_enable(bool): Enable/Disable reporting
failures to chrome perf dashboard
automatically. (Default: True)
_test_failure_report_subtest(bool): Enable/Disable reporting
subtests failure to chrome perf
dashboard automatically.
(Default: False)
"""
version = 1
_GSC = None
_test_failure_description = "Failures"
_test_failure_report_enable = True
_test_failure_report_subtest = False
def __init__(self, *args, **kwargs):
"""Initialize flag setting."""
super(GraphicsTest, self).__init__(*args, **kwargs)
self._failures_by_description = {}
self._player = None
def initialize(self, raise_error_on_hang=False, *args, **kwargs):
"""Initial state checker and report initial value to perf dashboard."""
self._GSC = GraphicsStateChecker(
raise_error_on_hang=raise_error_on_hang,
run_on_sw_rasterizer=utils.is_virtual_machine())
self.output_perf_value(
description='Timeout_Reboot',
value=1,
units='count',
higher_is_better=False,
replace_existing_values=True
)
if hasattr(super(GraphicsTest, self), "initialize"):
utils.cherry_pick_call(super(GraphicsTest, self).initialize,
*args, **kwargs)
def input_check(self):
"""Check if it exists and initialize input player."""
if self._player is None:
self._player = input_playback.InputPlayback()
self._player.emulate(input_type='keyboard')
self._player.find_connected_inputs()
def cleanup(self, *args, **kwargs):
"""Finalize state checker and report values to perf dashboard."""
if self._GSC:
self._GSC.finalize()
self._output_perf()
if self._player:
self._player.close()
if hasattr(super(GraphicsTest, self), "cleanup"):
utils.cherry_pick_call(super(GraphicsTest, self).cleanup,
*args, **kwargs)
@contextlib.contextmanager
def failure_report(self, name, subtest=None):
"""Record the failure of an operation to self._failures_by_description.
Records if the operation taken inside executed normally or not.
If the operation taken inside raise unexpected failure, failure named
|name|, will be added to the self._failures_by_description dictionary
and reported to the chrome perf dashboard in the cleanup stage.
Usage:
# Record failure of doSomething
with failure_report('doSomething'):
doSomething()
"""
# Assume failed at the beginning
self.add_failures(name, subtest=subtest)
try:
yield {}
self.remove_failures(name, subtest=subtest)
except (error.TestWarn, error.TestNAError) as e:
self.remove_failures(name, subtest=subtest)
raise e
@classmethod
def failure_report_decorator(cls, name, subtest=None):
"""Record the failure if the function failed to finish.
This method should only decorate to functions of GraphicsTest.
In addition, functions with this decorator should be called with no
unnamed arguments.
Usage:
@GraphicsTest.test_run_decorator('graphics_test')
def Foo(self, bar='test'):
return doStuff()
is equivalent to
def Foo(self, bar):
with failure_reporter('graphics_test'):
return doStuff()
# Incorrect usage.
@GraphicsTest.test_run_decorator('graphics_test')
def Foo(self, bar='test'):
pass
self.Foo('test_name', bar='test_name') # call Foo with named args
# Incorrect usage.
@GraphicsTest.test_run_decorator('graphics_test')
def Foo(self, bar='test'):
pass
self.Foo('test_name') # call Foo with unnamed args
"""
def decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
if len(args) > 1:
raise error.TestError('Unnamed arguments is not accepted. '
'Please apply this decorator to '
'function without unnamed args.')
# A member function of GraphicsTest is decorated. The first
# argument is the instance itself.
instance = args[0]
with instance.failure_report(name, subtest):
# Cherry pick the arguments for the wrapped function.
d_args, d_kwargs = utils.cherry_pick_args(fn, args, kwargs)
return fn(instance, *d_args, **d_kwargs)
return wrapper
return decorator
def add_failures(self, name, subtest=None):
"""
Add a record to failures list which will report back to chrome perf
dashboard at cleanup stage.
Args:
name: failure name.
subtest: subtest which will appears in cros-perf. If None is
specified, use name instead.
"""
target = self._get_failure(name, subtest=subtest)
if target:
target['names'].append(name)
else:
target = {
'description': self._get_failure_description(name, subtest),
'unit': 'count',
'higher_is_better': False,
'graph': self._get_failure_graph_name(),
'names': [name],
}
self._failures_by_description[target['description']] = target
return target
def remove_failures(self, name, subtest=None):
"""
Remove a record from failures list which will report back to chrome perf
dashboard at cleanup stage.
Args:
name: failure name.
subtest: subtest which will appears in cros-perf. If None is
specified, use name instead.
"""
target = self._get_failure(name, subtest=subtest)
if name in target['names']:
target['names'].remove(name)
def _output_perf(self):
"""Report recorded failures back to chrome perf."""
self.output_perf_value(
description='Timeout_Reboot',
value=0,
units='count',
higher_is_better=False,
replace_existing_values=True
)
if not self._test_failure_report_enable:
return
total_failures = 0
# Report subtests failures
for failure in self._failures_by_description.values():
if len(failure['names']) > 0:
logging.debug('GraphicsTest failure: %s' % failure['names'])
total_failures += len(failure['names'])
if not self._test_failure_report_subtest:
continue
self.output_perf_value(
description=failure['description'],
value=len(failure['names']),
units=failure['unit'],
higher_is_better=failure['higher_is_better'],
graph=failure['graph']
)
# Report the count of all failures
self.output_perf_value(
description=self._get_failure_graph_name(),
value=total_failures,
units='count',
higher_is_better=False,
)
def _get_failure_graph_name(self):
return self._test_failure_description
def _get_failure_description(self, name, subtest):
return subtest or name
def _get_failure(self, name, subtest):
"""Get specific failures."""
description = self._get_failure_description(name, subtest=subtest)
return self._failures_by_description.get(description, None)
def get_failures(self):
"""
Get currently recorded failures list.
"""
return [name for failure in self._failures_by_description.values()
for name in failure['names']]
def open_vt1(self):
"""Switch to VT1 with keyboard."""
self.input_check()
self._player.blocking_playback_of_default_file(
input_type='keyboard', filename='keyboard_ctrl+alt+f1')
time.sleep(5)
def open_vt2(self):
"""Switch to VT2 with keyboard."""
self.input_check()
self._player.blocking_playback_of_default_file(
input_type='keyboard', filename='keyboard_ctrl+alt+f2')
time.sleep(5)
def wake_screen_with_keyboard(self):
"""Use the vt1 keyboard shortcut to bring the devices screen back on.
This is useful if you want to take screenshots of the UI. If you try
to take them while the screen is off, it will fail.
"""
self.open_vt1()
def screen_disable_blanking():
""" Called from power_Backlight to disable screen blanking. """
# We don't have to worry about unexpected screensavers or DPMS here.
return
def screen_disable_energy_saving():
""" Called from power_Consumption to immediately disable energy saving. """
# All we need to do here is enable displays via Chrome.
power_utils.set_display_power(power_utils.DISPLAY_POWER_ALL_ON)
return
def screen_toggle_fullscreen():
"""Toggles fullscreen mode."""
press_keys(['KEY_F11'])
def screen_toggle_mirrored():
"""Toggles the mirrored screen."""
press_keys(['KEY_LEFTCTRL', 'KEY_F4'])
def hide_cursor():
"""Hides mouse cursor."""
# Send a keystroke to hide the cursor.
press_keys(['KEY_UP'])
def hide_typing_cursor():
"""Hides typing cursor."""
# Press the tab key to move outside the typing bar.
press_keys(['KEY_TAB'])
def screen_wakeup():
"""Wake up the screen if it is dark."""
# Move the mouse a little bit to wake up the screen.
device = graphics_uinput.get_device_mouse_rel()
graphics_uinput.emit(device, 'REL_X', 1)
graphics_uinput.emit(device, 'REL_X', -1)
def switch_screen_on(on):
"""
Turn the touch screen on/off.
@param on: On or off.
"""
raise error.TestFail('switch_screen_on is not implemented.')
def press_keys(key_list):
"""Presses the given keys as one combination.
Please do not leak uinput dependencies outside of the file.
@param key: A list of key strings, e.g. ['LEFTCTRL', 'F4']
"""
graphics_uinput.emit_combo(graphics_uinput.get_device_keyboard(), key_list)
def click_mouse():
"""Just click the mouse.
Presumably only hacky tests use this function.
"""
logging.info('click_mouse()')
# Move a little to make the cursor appear.
device = graphics_uinput.get_device_mouse_rel()
graphics_uinput.emit(device, 'REL_X', 1)
# Some sleeping is needed otherwise events disappear.
time.sleep(0.1)
# Move cursor back to not drift.
graphics_uinput.emit(device, 'REL_X', -1)
time.sleep(0.1)
# Click down.
graphics_uinput.emit(device, 'BTN_LEFT', 1)
time.sleep(0.2)
# Release click.
graphics_uinput.emit(device, 'BTN_LEFT', 0)
# TODO(ihf): this function is broken. Make it work.
def activate_focus_at(rel_x, rel_y):
"""Clicks with the mouse at screen position (x, y).
This is a pretty hacky method. Using this will probably lead to
flaky tests as page layout changes over time.
@param rel_x: relative horizontal position between 0 and 1.
@param rel_y: relattive vertical position between 0 and 1.
"""
width, height = get_internal_resolution()
device = graphics_uinput.get_device_touch()
graphics_uinput.emit(device, 'ABS_MT_SLOT', 0, syn=False)
graphics_uinput.emit(device, 'ABS_MT_TRACKING_ID', 1, syn=False)
graphics_uinput.emit(device, 'ABS_MT_POSITION_X', int(rel_x * width),
syn=False)
graphics_uinput.emit(device, 'ABS_MT_POSITION_Y', int(rel_y * height),
syn=False)
graphics_uinput.emit(device, 'BTN_TOUCH', 1, syn=True)
time.sleep(0.2)
graphics_uinput.emit(device, 'BTN_TOUCH', 0, syn=True)
def take_screenshot(resultsdir, fname_prefix):
"""Take screenshot and save to a new file in the results dir.
Args:
@param resultsdir: Directory to store the output in.
@param fname_prefix: Prefix for the output fname.
Returns:
the path of the saved screenshot file
"""
old_exc_type = sys.exc_info()[0]
next_index = len(glob.glob(
os.path.join(resultsdir, '%s-*.png' % fname_prefix)))
screenshot_file = os.path.join(
resultsdir, '%s-%d.png' % (fname_prefix, next_index))
logging.info('Saving screenshot to %s.', screenshot_file)
try:
utils.run('screenshot "%s"' % screenshot_file)
except Exception as err:
# Do not raise an exception if the screenshot fails while processing
# another exception.
if old_exc_type is None:
raise
logging.error(err)
return screenshot_file
def take_screenshot_crop(fullpath, box=None, crtc_id=None):
"""
Take a screenshot using import tool, crop according to dim given by the box.
@param fullpath: path, full path to save the image to.
@param box: 4-tuple giving the upper left and lower right pixel coordinates.
@param crtc_id: if set, take a screen shot of the specified CRTC.
"""
cmd = 'screenshot'
if crtc_id is not None:
cmd += ' --crtc-id=%d' % crtc_id
else:
cmd += ' --internal'
if box:
x, y, r, b = box
w = r - x
h = b - y
cmd += ' --crop=%dx%d+%d+%d' % (w, h, x, y)
cmd += ' "%s"' % fullpath
utils.run(cmd)
return fullpath
# id encoder status name size (mm) modes encoders
# 39 0 connected eDP-1 256x144 1 38
_MODETEST_CONNECTOR_PATTERN = re.compile(
r'^(\d+)\s+(\d+)\s+(connected|disconnected)\s+(\S+)\s+\d+x\d+\s+\d+\s+\d+')
# id crtc type possible crtcs possible clones
# 38 0 TMDS 0x00000002 0x00000000
_MODETEST_ENCODER_PATTERN = re.compile(
r'^(\d+)\s+(\d+)\s+\S+\s+0x[0-9a-fA-F]+\s+0x[0-9a-fA-F]+')
# Group names match the drmModeModeInfo struct
_MODETEST_MODE_PATTERN = re.compile(
r'\s+(?P<name>.+)'
r'\s+(?P<vrefresh>\d+)'
r'\s+(?P<hdisplay>\d+)'
r'\s+(?P<hsync_start>\d+)'
r'\s+(?P<hsync_end>\d+)'
r'\s+(?P<htotal>\d+)'
r'\s+(?P<vdisplay>\d+)'
r'\s+(?P<vsync_start>\d+)'
r'\s+(?P<vsync_end>\d+)'
r'\s+(?P<vtotal>\d+)'
r'\s+(?P<clock>\d+)'
r'\s+flags:.+type:'
r' preferred')
_MODETEST_CRTCS_START_PATTERN = re.compile(r'^id\s+fb\s+pos\s+size')
_MODETEST_CRTC_PATTERN = re.compile(
r'^(\d+)\s+(\d+)\s+\((\d+),(\d+)\)\s+\((\d+)x(\d+)\)')
_MODETEST_PLANES_START_PATTERN = re.compile(
r'^id\s+crtc\s+fb\s+CRTC\s+x,y\s+x,y\s+gamma\s+size\s+possible\s+crtcs')
_MODETEST_PLANE_PATTERN = re.compile(
r'^(\d+)\s+(\d+)\s+(\d+)\s+(\d+),(\d+)\s+(\d+),(\d+)\s+(\d+)\s+(0x)(\d+)')
Connector = collections.namedtuple(
'Connector', [
'cid', # connector id (integer)
'eid', # encoder id (integer)
'ctype', # connector type, e.g. 'eDP', 'HDMI-A', 'DP'
'connected', # boolean
'size', # current screen size in mm, e.g. (256, 144)
'encoder', # encoder id (integer)
# list of resolution tuples, e.g. [(1920,1080), (1600,900), ...]
'modes',
])
Encoder = collections.namedtuple(
'Encoder', [
'eid', # encoder id (integer)
'crtc_id', # CRTC id (integer)
])
CRTC = collections.namedtuple(
'CRTC', [
'id', # crtc id
'fb', # fb id
'pos', # position, e.g. (0,0)
'size', # size, e.g. (1366,768)
'is_internal', # True if for the internal display
])
Plane = collections.namedtuple(
'Plane', [
'id', # plane id
'possible_crtcs', # possible associated CRTC indexes.
])
def get_display_resolution():
"""
Parses output of modetest to determine the display resolution of the dut.
@return: tuple, (w,h) resolution of device under test.
"""
connectors = get_modetest_connectors()
for connector in connectors:
if connector.connected:
return connector.size
return None
def _get_num_outputs_connected():
"""
Parses output of modetest to determine the number of connected displays
@return: The number of connected displays
"""
connected = 0
connectors = get_modetest_connectors()
for connector in connectors:
if connector.connected:
connected = connected + 1
return connected
def get_num_outputs_on():
"""
Retrieves the number of connected outputs that are on.
Return value: integer value of number of connected outputs that are on.
"""
return _get_num_outputs_connected()
def get_modetest_connectors():
"""
Retrieves a list of Connectors using modetest.
Return value: List of Connectors.
"""
connectors = []
modetest_output = utils.system_output('modetest -c')
for line in modetest_output.splitlines():
# First search for a new connector.
connector_match = re.match(_MODETEST_CONNECTOR_PATTERN, line)
if connector_match is not None:
cid = int(connector_match.group(1))
eid = int(connector_match.group(2))
connected = False
if connector_match.group(3) == 'connected':
connected = True
ctype = connector_match.group(4)
size = (-1, -1)
encoder = -1
modes = None
connectors.append(
Connector(cid, eid, ctype, connected, size, encoder, modes))
else:
# See if we find corresponding line with modes, sizes etc.
mode_match = re.match(_MODETEST_MODE_PATTERN, line)
if mode_match is not None:
size = (int(mode_match.group('hdisplay')),
int(mode_match.group('vdisplay')))
# Update display size of last connector in list.
c = connectors.pop()
connectors.append(
Connector(
c.cid, c.eid, c.ctype, c.connected, size, c.encoder,
c.modes))
return connectors
def get_modetest_encoders():
"""
Retrieves a list of Encoders using modetest.
Return value: List of Encoders.
"""
encoders = []
modetest_output = utils.system_output('modetest -e')
for line in modetest_output.splitlines():
encoder_match = re.match(_MODETEST_ENCODER_PATTERN, line)
if encoder_match is None:
continue
eid = int(encoder_match.group(1))
crtc_id = int(encoder_match.group(2))
encoders.append(Encoder(eid, crtc_id))
return encoders
def find_eid_from_crtc_id(crtc_id):
"""
Finds the integer Encoder ID matching a CRTC ID.
@param crtc_id: The integer CRTC ID.
@return: The integer Encoder ID or None.
"""
encoders = get_modetest_encoders()
for encoder in encoders:
if encoder.crtc_id == crtc_id:
return encoder.eid
return None
def find_connector_from_eid(eid):
"""
Finds the Connector object matching an Encoder ID.
@param eid: The integer Encoder ID.
@return: The Connector object or None.
"""
connectors = get_modetest_connectors()
for connector in connectors:
if connector.eid == eid:
return connector
return None
def get_modetest_crtcs():
"""
Returns a list of CRTC data.
Sample:
[CRTC(id=19, fb=50, pos=(0, 0), size=(1366, 768)),
CRTC(id=22, fb=54, pos=(0, 0), size=(1920, 1080))]
"""
crtcs = []
modetest_output = utils.system_output('modetest -p')
found = False
for line in modetest_output.splitlines():
if found:
crtc_match = re.match(_MODETEST_CRTC_PATTERN, line)
if crtc_match is not None:
crtc_id = int(crtc_match.group(1))
fb = int(crtc_match.group(2))
x = int(crtc_match.group(3))
y = int(crtc_match.group(4))
width = int(crtc_match.group(5))
height = int(crtc_match.group(6))
# CRTCs with fb=0 are disabled, but lets skip anything with
# trivial width/height just in case.
if not (fb == 0 or width == 0 or height == 0):
eid = find_eid_from_crtc_id(crtc_id)
connector = find_connector_from_eid(eid)
if connector is None:
is_internal = False
else:
is_internal = (connector.ctype ==
get_internal_connector_name())
crtcs.append(CRTC(crtc_id, fb, (x, y), (width, height),
is_internal))
elif line and not line[0].isspace():
return crtcs
if re.match(_MODETEST_CRTCS_START_PATTERN, line) is not None:
found = True
return crtcs
def get_modetest_planes():
"""
Returns a list of planes information.
Sample:
[Plane(id=26, possible_crtcs=1),
Plane(id=29, possible_crtcs=1)]
"""
planes = []
modetest_output = utils.system_output('modetest -p')
found = False
for line in modetest_output.splitlines():
if found:
plane_match = re.match(_MODETEST_PLANE_PATTERN, line)
if plane_match is not None:
plane_id = int(plane_match.group(1))
possible_crtcs = int(plane_match.group(10))
if not (plane_id == 0 or possible_crtcs == 0):
planes.append(Plane(plane_id, possible_crtcs))
elif line and not line[0].isspace():
return planes
if re.match(_MODETEST_PLANES_START_PATTERN, line) is not None:
found = True
return planes
def is_nv12_supported_by_drm_planes():
"""
Returns if the planes information mention NV12 format or not.
This is a crude way to figure out if the device will not be able to promote
video frames to overlays at all, which happens for example on Broadwell.
"""
modetest_output = utils.system_output('modetest -p')
return "nv12" in modetest_output.lower()
def get_modetest_output_state():
"""
Reduce the output of get_modetest_connectors to a dictionary of connector/active states.
"""
connectors = get_modetest_connectors()
outputs = {}
for connector in connectors:
# TODO(ihf): Figure out why modetest output needs filtering.
if connector.connected:
outputs[connector.ctype] = connector.connected
return outputs
def get_output_rect(output):
"""Gets the size and position of the given output on the screen buffer.
@param output: The output name as a string.
@return A tuple of the rectangle (width, height, fb_offset_x,
fb_offset_y) of ints.
"""
connectors = get_modetest_connectors()
for connector in connectors:
if connector.ctype == output:
# Concatenate two 2-tuples to 4-tuple.
return connector.size + (0, 0) # TODO(ihf): Should we use CRTC.pos?
return (0, 0, 0, 0)
def get_internal_crtc():
for crtc in get_modetest_crtcs():
if crtc.is_internal:
return crtc
return None
def get_external_crtc(index=0):
for crtc in get_modetest_crtcs():
if not crtc.is_internal:
if index == 0:
return crtc
index -= 1
return None
def get_internal_resolution():
crtc = get_internal_crtc()
if crtc:
return crtc.size
return (-1, -1)
def has_internal_display():
"""Checks whether the DUT is equipped with an internal display.
@return True if internal display is present; False otherwise.
"""
return bool(get_internal_connector_name())
def get_external_resolution():
"""Gets the resolution of the external display.
@return A tuple of (width, height) or None if no external display is
connected.
"""
crtc = get_external_crtc()
if crtc:
return crtc.size
return None
def get_display_output_state():
"""
Retrieves output status of connected display(s).
Return value: dictionary of connected display states.
"""
return get_modetest_output_state()
def set_modetest_output(output_name, enable):
# TODO(ihf): figure out what to do here. Don't think this is the right command.
# modetest -s <connector_id>[,<connector_id>][@<crtc_id>]:<mode>[-<vrefresh>][@<format>] set a mode
pass
def set_display_output(output_name, enable):
"""
Sets the output given by |output_name| on or off.
"""
set_modetest_output(output_name, enable)
# TODO(ihf): Fix this for multiple external connectors.
def get_external_crtc_id(index=0):
crtc = get_external_crtc(index)
if crtc is not None:
return crtc.id
return -1
def get_internal_crtc_id():
crtc = get_internal_crtc()
if crtc is not None:
return crtc.id
return -1
# TODO(ihf): Fix this for multiple external connectors.
def get_external_connector_name():
"""Gets the name of the external output connector.
@return The external output connector name as a string, if any.
Otherwise, return False.
"""
outputs = get_display_output_state()
for output in outputs.iterkeys():
if outputs[output] and (output.startswith('HDMI')
or output.startswith('DP')
or output.startswith('DVI')
or output.startswith('VGA')):
return output
return False
def get_internal_connector_name():
"""Gets the name of the internal output connector.
@return The internal output connector name as a string, if any.
Otherwise, return False.
"""
outputs = get_display_output_state()
for output in outputs.iterkeys():
# reference: chromium_org/chromeos/display/output_util.cc
if (output.startswith('eDP')
or output.startswith('LVDS')
or output.startswith('DSI')):
return output
return False
def wait_output_connected(output):
"""Wait for output to connect.
@param output: The output name as a string.
@return: True if output is connected; False otherwise.
"""
def _is_connected(output):
"""Helper function."""
outputs = get_display_output_state()
if output not in outputs:
return False
return outputs[output]
return utils.wait_for_value(lambda: _is_connected(output),
expected_value=True)
def set_content_protection(output_name, state):
"""
Sets the content protection to the given state.
@param output_name: The output name as a string.
@param state: One of the states 'Undesired', 'Desired', or 'Enabled'
"""
raise error.TestFail('freon: set_content_protection not implemented')
def get_content_protection(output_name):
"""
Gets the state of the content protection.
@param output_name: The output name as a string.
@return: A string of the state, like 'Undesired', 'Desired', or 'Enabled'.
False if not supported.
"""
raise error.TestFail('freon: get_content_protection not implemented')
def is_sw_rasterizer():
"""Return true if OpenGL is using a software rendering."""
cmd = utils.wflinfo_cmd() + ' | grep "OpenGL renderer string"'
output = utils.run(cmd)
result = output.stdout.splitlines()[0]
logging.info('wflinfo: %s', result)
# TODO(ihf): Find exhaustive error conditions (especially ARM).
return 'llvmpipe' in result.lower() or 'soft' in result.lower()
def get_gles_version():
cmd = utils.wflinfo_cmd()
wflinfo = utils.system_output(cmd, retain_output=False, ignore_status=False)
# OpenGL version string: OpenGL ES 3.0 Mesa 10.5.0-devel
version = re.findall(r'OpenGL version string: '
r'OpenGL ES ([0-9]+).([0-9]+)', wflinfo)
if version:
version_major = int(version[0][0])
version_minor = int(version[0][1])
return (version_major, version_minor)
return (None, None)
def get_egl_version():
cmd = 'eglinfo'
eglinfo = utils.system_output(cmd, retain_output=False, ignore_status=False)
# EGL version string: 1.4 (DRI2)
version = re.findall(r'EGL version string: ([0-9]+).([0-9]+)', eglinfo)
if version:
version_major = int(version[0][0])
version_minor = int(version[0][1])
return (version_major, version_minor)
return (None, None)
class GraphicsKernelMemory(object):
"""
Reads from sysfs to determine kernel gem objects and memory info.
"""
# These are sysfs fields that will be read by this test. For different
# architectures, the sysfs field paths are different. The "paths" are given
# as lists of strings because the actual path may vary depending on the
# system. This test will read from the first sysfs path in the list that is
# present.
# e.g. ".../memory" vs ".../gpu_memory" -- if the system has either one of
# these, the test will read from that path.
amdgpu_fields = {
'gem_objects': ['/sys/kernel/debug/dri/0/amdgpu_gem_info'],
'memory': ['/sys/kernel/debug/dri/0/amdgpu_gtt_mm'],
}
arm_fields = {}
exynos_fields = {
'gem_objects': ['/sys/kernel/debug/dri/?/exynos_gem_objects'],
'memory': ['/sys/class/misc/mali0/device/memory',
'/sys/class/misc/mali0/device/gpu_memory'],
}
mediatek_fields = {}
# TODO(crosbug.com/p/58189) Add mediatek GPU memory nodes
qualcomm_fields = {}
# TODO(b/119269602) Add qualcomm GPU memory nodes once GPU patches land
rockchip_fields = {}
tegra_fields = {
'memory': ['/sys/kernel/debug/memblock/memory'],
}
i915_fields = {
'gem_objects': ['/sys/kernel/debug/dri/0/i915_gem_objects'],
'memory': ['/sys/kernel/debug/dri/0/i915_gem_gtt'],
}
# In Linux Kernel 5, i915_gem_gtt merged into i915_gem_objects
i915_fields_kernel_5 = {
'gem_objects': ['/sys/kernel/debug/dri/0/i915_gem_objects'],
}
cirrus_fields = {}
virtio_fields = {}
arch_fields = {
'amdgpu': amdgpu_fields,
'arm': arm_fields,
'cirrus': cirrus_fields,
'exynos5': exynos_fields,
'i915': i915_fields,
'i915_kernel_5': i915_fields_kernel_5,
'mediatek': mediatek_fields,
'qualcomm': qualcomm_fields,
'rockchip': rockchip_fields,
'tegra': tegra_fields,
'virtio': virtio_fields,
}
num_errors = 0
def __init__(self):
self._initial_memory = self.get_memory_keyvals()
def get_memory_difference_keyvals(self):
"""
Reads the graphics memory values and return the difference between now
and the memory usage at initialization stage as keyvals.
"""
current_memory = self.get_memory_keyvals()
return {key: self._initial_memory[key] - current_memory[key]
for key in self._initial_memory}
def get_memory_keyvals(self):
"""
Reads the graphics memory values and returns them as keyvals.
"""
keyvals = {}
# Get architecture type and list of sysfs fields to read.
soc = utils.get_cpu_soc_family()
arch = utils.get_cpu_arch()
kernel_version = utils.get_kernel_version()[0:4].rstrip(".")
if arch == 'x86_64' or arch == 'i386':
pci_vga_device = utils.run("lspci | grep VGA").stdout.rstrip('\n')
if "Advanced Micro Devices" in pci_vga_device:
soc = 'amdgpu'
elif "Intel Corporation" in pci_vga_device:
soc = 'i915'
if utils.compare_versions(kernel_version, "4.19") > 0:
soc = 'i915_kernel_5'
elif "Cirrus Logic" in pci_vga_device:
# Used on qemu with kernels 3.18 and lower. Limited to 800x600
# resolution.
soc = 'cirrus'
else:
pci_vga_device = utils.run('lshw -c video').stdout.rstrip()
groups = re.search('configuration:.*driver=(\S*)',
pci_vga_device)
if groups and 'virtio' in groups.group(1):
soc = 'virtio'
if not soc in self.arch_fields:
raise error.TestFail('Error: Architecture "%s" not yet supported.' % soc)
fields = self.arch_fields[soc]
for field_name in fields:
possible_field_paths = fields[field_name]
field_value = None
for path in possible_field_paths:
if utils.system('ls %s' % path, ignore_status=True):
continue
field_value = utils.system_output('cat %s' % path)
break
if not field_value:
logging.error('Unable to find any sysfs paths for field "%s"',
field_name)
self.num_errors += 1
continue
parsed_results = GraphicsKernelMemory._parse_sysfs(field_value)
for key in parsed_results:
keyvals['%s_%s' % (field_name, key)] = parsed_results[key]
if 'bytes' in parsed_results and parsed_results['bytes'] == 0:
logging.error('%s reported 0 bytes', field_name)
self.num_errors += 1
keyvals['meminfo_MemUsed'] = (utils.read_from_meminfo('MemTotal') -
utils.read_from_meminfo('MemFree'))
keyvals['meminfo_SwapUsed'] = (utils.read_from_meminfo('SwapTotal') -
utils.read_from_meminfo('SwapFree'))
return keyvals
@staticmethod
def _parse_sysfs(output):
"""
Parses output of graphics memory sysfs to determine the number of
buffer objects and bytes.
Arguments:
output Unprocessed sysfs output
Return value:
Dictionary containing integer values of number bytes and objects.
They may have the keys 'bytes' and 'objects', respectively. However
the result may not contain both of these values.
"""
results = {}
labels = ['bytes', 'objects']
# First handle i915_gem_objects in 5.x kernels. Example:
# 296 shrinkable [0 free] objects, 274833408 bytes
# frecon: 3 objects, 72192000 bytes (0 active, 0 inactive, 0 unbound, 0 closed)
# chrome: 6 objects, 74629120 bytes (0 active, 0 inactive, 376832 unbound, 0 closed)
# <snip>
i915_gem_objects_pattern = re.compile(
r'(?P<objects>\d*) shrinkable.*objects, (?P<bytes>\d*) bytes')
i915_gem_objects_match = i915_gem_objects_pattern.match(output)
if i915_gem_objects_match is not None:
results['bytes'] = int(i915_gem_objects_match.group('bytes'))
results['objects'] = int(i915_gem_objects_match.group('objects'))
return results
for line in output.split('\n'):
# Strip any commas to make parsing easier.
line_words = line.replace(',', '').split()
prev_word = None
for word in line_words:
# When a label has been found, the previous word should be the
# value. e.g. "3200 bytes"
if word in labels and word not in results and prev_word:
logging.info(prev_word)
results[word] = int(prev_word)
prev_word = word
# Once all values has been parsed, return.
if len(results) == len(labels):
return results
return results
class GraphicsStateChecker(object):
"""
Analyzes the state of the GPU and log history. Should be instantiated at the
beginning of each graphics_* test.
"""
dirty_writeback_centisecs = 0
existing_hangs = {}
_BROWSER_VERSION_COMMAND = '/opt/google/chrome/chrome --version'
_HANGCHECK = ['drm:i915_hangcheck_elapsed', 'drm:i915_hangcheck_hung',
'Hangcheck timer elapsed...',
'drm/i915: Resetting chip after gpu hang']
_HANGCHECK_WARNING = ['render ring idle']
_MESSAGES_FILE = '/var/log/messages'
def __init__(self, raise_error_on_hang=True, run_on_sw_rasterizer=False):
"""
Analyzes the initial state of the GPU and log history.
"""
# Attempt flushing system logs every second instead of every 10 minutes.
self.dirty_writeback_centisecs = utils.get_dirty_writeback_centisecs()
utils.set_dirty_writeback_centisecs(100)
self._raise_error_on_hang = raise_error_on_hang
logging.info(utils.get_board_with_frequency_and_memory())
self.graphics_kernel_memory = GraphicsKernelMemory()
self._run_on_sw_rasterizer = run_on_sw_rasterizer
if utils.get_cpu_arch() != 'arm':
if not self._run_on_sw_rasterizer and is_sw_rasterizer():
raise error.TestFail('Refusing to run on SW rasterizer.')
logging.info('Initialize: Checking for old GPU hangs...')
messages = open(self._MESSAGES_FILE, 'r')
for line in messages:
for hang in self._HANGCHECK:
if hang in line:
logging.info(line)
self.existing_hangs[line] = line
messages.close()
def finalize(self):
"""
Analyzes the state of the GPU, log history and emits warnings or errors
if the state changed since initialize. Also makes a note of the Chrome
version for later usage in the perf-dashboard.
"""
utils.set_dirty_writeback_centisecs(self.dirty_writeback_centisecs)
new_gpu_hang = False
new_gpu_warning = False
if utils.get_cpu_arch() != 'arm':
logging.info('Cleanup: Checking for new GPU hangs...')
messages = open(self._MESSAGES_FILE, 'r')
for line in messages:
for hang in self._HANGCHECK:
if hang in line:
if not line in self.existing_hangs.keys():
logging.info(line)
for warn in self._HANGCHECK_WARNING:
if warn in line:
new_gpu_warning = True
logging.warning(
'Saw GPU hang warning during test.')
else:
logging.warning('Saw GPU hang during test.')
new_gpu_hang = True
messages.close()
if not self._run_on_sw_rasterizer and is_sw_rasterizer():
logging.warning('Finished test on SW rasterizer.')
raise error.TestFail('Finished test on SW rasterizer.')
if self._raise_error_on_hang and new_gpu_hang:
raise error.TestError('Detected GPU hang during test.')
if new_gpu_hang:
raise error.TestWarn('Detected GPU hang during test.')
if new_gpu_warning:
raise error.TestWarn('Detected GPU warning during test.')
def get_memory_access_errors(self):
""" Returns the number of errors while reading memory stats. """
return self.graphics_kernel_memory.num_errors
def get_memory_difference_keyvals(self):
return self.graphics_kernel_memory.get_memory_difference_keyvals()
def get_memory_keyvals(self):
""" Returns memory stats. """
return self.graphics_kernel_memory.get_memory_keyvals()
class GraphicsApiHelper(object):
"""
Report on the available graphics APIs.
Ex. gles2, gles3, gles31, and vk
"""
_supported_apis = []
DEQP_BASEDIR = os.path.join('/usr', 'local', 'deqp')
DEQP_EXECUTABLE = {
'gles2': os.path.join('modules', 'gles2', 'deqp-gles2'),
'gles3': os.path.join('modules', 'gles3', 'deqp-gles3'),
'gles31': os.path.join('modules', 'gles31', 'deqp-gles31'),
'vk': os.path.join('external', 'vulkancts', 'modules',
'vulkan', 'deqp-vk')
}
def __init__(self):
# Determine which executable should be run. Right now never egl.
major, minor = get_gles_version()
logging.info('Found gles%d.%d.', major, minor)
if major is None or minor is None:
raise error.TestFail(
'Failed: Could not get gles version information (%d, %d).' %
(major, minor)
)
if major >= 2:
self._supported_apis.append('gles2')
if major >= 3:
self._supported_apis.append('gles3')
if major > 3 or minor >= 1:
self._supported_apis.append('gles31')
# If libvulkan is installed, then assume the board supports vulkan.
has_libvulkan = False
for libdir in ('/usr/lib', '/usr/lib64',
'/usr/local/lib', '/usr/local/lib64'):
if os.path.exists(os.path.join(libdir, 'libvulkan.so')):
has_libvulkan = True
if has_libvulkan:
executable_path = os.path.join(
self.DEQP_BASEDIR,
self.DEQP_EXECUTABLE['vk']
)
if os.path.exists(executable_path):
self._supported_apis.append('vk')
else:
logging.warning('Found libvulkan.so but did not find deqp-vk '
'binary for testing.')
def get_supported_apis(self):
"""Return the list of supported apis. eg. gles2, gles3, vk etc.
@returns: a copy of the supported api list will be returned
"""
return list(self._supported_apis)
def get_deqp_executable(self, api):
"""Return the path to the api executable."""
if api not in self.DEQP_EXECUTABLE:
raise KeyError(
"%s is not a supported api for GraphicsApiHelper." % api
)
executable = os.path.join(
self.DEQP_BASEDIR,
self.DEQP_EXECUTABLE[api]
)
return executable
# Possible paths of the kernel DRI debug text file.
_DRI_DEBUG_FILE_PATH_0 = "/sys/kernel/debug/dri/0/state"
_DRI_DEBUG_FILE_PATH_1 = "/sys/kernel/debug/dri/1/state"
_DRI_DEBUG_FILE_PATH_2 = "/sys/kernel/debug/dri/2/state"
# The DRI debug file will have a lot of information, including the position and
# sizes of each plane. Some planes might be disabled but have some lingering
# crtc-pos information, those are skipped.
_CRTC_PLANE_START_PATTERN = re.compile(r'plane\[')
_CRTC_DISABLED_PLANE = re.compile(r'crtc=\(null\)')
_CRTC_POS_AND_SIZE_PATTERN = re.compile(r'crtc-pos=(?!0x0\+0\+0)')
def get_num_hardware_overlays():
"""
Counts the amount of hardware overlay planes in use. There's always at
least 2 overlays active: the whole screen and the cursor -- unless the
cursor has never moved (e.g. in autotests), and it's not present.
Raises: RuntimeError if the DRI debug file is not present.
OSError/IOError if the file cannot be open()ed or read().
"""
file_path = _DRI_DEBUG_FILE_PATH_0;
if os.path.exists(_DRI_DEBUG_FILE_PATH_0):
file_path = _DRI_DEBUG_FILE_PATH_0;
elif os.path.exists(_DRI_DEBUG_FILE_PATH_1):
file_path = _DRI_DEBUG_FILE_PATH_1;
elif os.path.exists(_DRI_DEBUG_FILE_PATH_2):
file_path = _DRI_DEBUG_FILE_PATH_2;
else:
raise RuntimeError('No DRI debug file exists (%s, %s)' %
(_DRI_DEBUG_FILE_PATH_0, _DRI_DEBUG_FILE_PATH_1))
filetext = open(file_path).read()
logging.debug(filetext)
matches = []
# Split the debug output by planes, skip the disabled ones and extract those
# with correct position and size information.
planes = re.split(_CRTC_PLANE_START_PATTERN, filetext)
for plane in planes:
if len(plane) == 0:
continue;
if len(re.findall(_CRTC_DISABLED_PLANE, plane)) > 0:
continue;
matches.append(re.findall(_CRTC_POS_AND_SIZE_PATTERN, plane))
# TODO(crbug.com/865112): return also the sizes/locations.
return len(matches)
def is_drm_debug_supported():
"""
@returns true if either of the DRI debug files are present.
"""
return (os.path.exists(_DRI_DEBUG_FILE_PATH_0) or
os.path.exists(_DRI_DEBUG_FILE_PATH_1) or
os.path.exists(_DRI_DEBUG_FILE_PATH_2))
# Path and file name regex defining the filesystem location for DRI devices.
_DEV_DRI_FOLDER_PATH = '/dev/dri'
_DEV_DRI_CARD_PATH = '/dev/dri/card?'
# IOCTL code and associated parameter to set the atomic cap. Defined originally
# in the kernel's include/uapi/drm/drm.h file.
_DRM_IOCTL_SET_CLIENT_CAP = 0x4010640d
_DRM_CLIENT_CAP_ATOMIC = 3
def is_drm_atomic_supported():
"""
@returns true if there is at least a /dev/dri/card? file that seems to
support drm_atomic mode (accepts a _DRM_IOCTL_SET_CLIENT_CAP ioctl).
"""
if not os.path.isdir(_DEV_DRI_FOLDER_PATH):
# This should never ever happen.
raise error.TestError('path %s inexistent', _DEV_DRI_FOLDER_PATH);
for dev_path in glob.glob(_DEV_DRI_CARD_PATH):
try:
logging.debug('trying device %s', dev_path);
with open(dev_path, 'rw') as dev:
# Pack a struct drm_set_client_cap: two u64.
drm_pack = struct.pack("QQ", _DRM_CLIENT_CAP_ATOMIC, 1)
result = fcntl.ioctl(dev, _DRM_IOCTL_SET_CLIENT_CAP, drm_pack)
if result is None or len(result) != len(drm_pack):
# This should never ever happen.
raise error.TestError('ioctl failure')
logging.debug('%s supports atomic', dev_path);
if not is_drm_debug_supported():
raise error.TestError('platform supports DRM but there '
' are no debug files for it')
return True
except IOError as err:
logging.warning('ioctl failed on %s: %s', dev_path, str(err));
logging.debug('No dev files seems to support atomic');
return False
def get_max_num_available_drm_planes():
"""
@returns The maximum number of DRM planes available in the system
(associated to the same CRTC), or 0 if something went wrong (e.g. modetest
failed, etc).
"""
planes = get_modetest_planes()
if len(planes) == 0:
return 0;
packed_possible_crtcs = [plane.possible_crtcs for plane in planes]
# |packed_possible_crtcs| is actually a bit field of possible CRTCs, e.g.
# 0x6 (b1001) means the plane can be associated with CRTCs index 0 and 3 but
# not with index 1 nor 2. Unpack those into |possible_crtcs|, an array of
# binary arrays.
possible_crtcs = [[int(bit) for bit in bin(crtc)[2:].zfill(16)]
for crtc in packed_possible_crtcs]
# Accumulate the CRTCs indexes and return the maximum number of 'votes'.
return max(map(sum, zip(*possible_crtcs)))