mirror of
				https://github.com/ARM-software/workload-automation.git
				synced 2025-11-04 00:52:08 +00:00 
			
		
		
		
	
							
								
								
									
										92
									
								
								wa/commands/record.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								wa/commands/record.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,92 @@
 | 
			
		||||
#    Copyright 2014-2015 ARM Limited
 | 
			
		||||
#
 | 
			
		||||
# Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
# you may not use this file except in compliance with the License.
 | 
			
		||||
# You may obtain a copy of the License at
 | 
			
		||||
#
 | 
			
		||||
#     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
#
 | 
			
		||||
# Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
# distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
# See the License for the specific language governing permissions and
 | 
			
		||||
# limitations under the License.
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
import sys
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
from wa import Command, settings
 | 
			
		||||
from wa.framework.configuration import RunConfiguration
 | 
			
		||||
from wa.framework.resource import Executable, NO_ONE, ResourceResolver
 | 
			
		||||
from wa.utils.revent import ReventRecorder
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RecordCommand(Command):
 | 
			
		||||
 | 
			
		||||
    name = 'record'
 | 
			
		||||
    description = '''Performs a revent recording
 | 
			
		||||
 | 
			
		||||
    This command helps making revent recordings. It will automatically
 | 
			
		||||
    deploy revent and even has the option of automatically opening apps.
 | 
			
		||||
 | 
			
		||||
    Revent allows you to record raw inputs such as screen swipes or button presses.
 | 
			
		||||
    This can be useful for recording inputs for workloads such as games that don't
 | 
			
		||||
    have XML UI layouts that can be used with UIAutomator. As a drawback from this,
 | 
			
		||||
    revent recordings are specific to the device type they were recorded on.
 | 
			
		||||
 | 
			
		||||
    WA uses two parts to the names of revent recordings in the format,
 | 
			
		||||
    {device_name}.{suffix}.revent.
 | 
			
		||||
 | 
			
		||||
     - device_name can either be specified manually with the ``-d`` argument or
 | 
			
		||||
       it can be automatically determined. On Android device it will be obtained
 | 
			
		||||
       from ``build.prop``, on Linux devices it is obtained from ``/proc/device-tree/model``.
 | 
			
		||||
     - suffix is used by WA to determine which part of the app execution the
 | 
			
		||||
       recording is for, currently these are either ``setup`` or ``run``. This
 | 
			
		||||
       should be specified with the ``-s`` argument.
 | 
			
		||||
    '''
 | 
			
		||||
 | 
			
		||||
    def initialize(self, context):
 | 
			
		||||
        self.parser.add_argument('-d', '--device', metavar='DEVICE',
 | 
			
		||||
                                 help='''
 | 
			
		||||
                                 Specify the device on which to run. This will
 | 
			
		||||
                                 take precedence over the device (if any)
 | 
			
		||||
                                 specified in configuration.
 | 
			
		||||
                                 ''')
 | 
			
		||||
 | 
			
		||||
    def execute(state, args):
 | 
			
		||||
        if args.device:
 | 
			
		||||
            device =  args.device
 | 
			
		||||
            device_config = {}
 | 
			
		||||
        else:
 | 
			
		||||
            device = state.run_config.device
 | 
			
		||||
            device_config = state.run_config.device_config
 | 
			
		||||
        target_manager = TargetManager(device, device_config)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_revent_binary(abi):
 | 
			
		||||
    resolver = ResourceResolver()
 | 
			
		||||
    resource = Executable(NO_ONE, abi, 'revent')
 | 
			
		||||
    return resolver.get(resource)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ReventRecorder(object):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, target):
 | 
			
		||||
        self.target = target
 | 
			
		||||
        self.executable = None
 | 
			
		||||
        self.deploy()
 | 
			
		||||
 | 
			
		||||
    def deploy(self):
 | 
			
		||||
        host_executable = get_revent_binary(self.target.abi)
 | 
			
		||||
        self.executable = self.target.install(host_executable)
 | 
			
		||||
 | 
			
		||||
    def record(self, path):
 | 
			
		||||
        name = os.path.basename(path)
 | 
			
		||||
        target_path = self.target.get_workpath(name)
 | 
			
		||||
        command = '{} record {}'
 | 
			
		||||
 | 
			
		||||
    def remove(self):
 | 
			
		||||
        if self.executable:
 | 
			
		||||
            self.target.uninstall('revent')
 | 
			
		||||
							
								
								
									
										17
									
								
								wa/tools/revent/Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								wa/tools/revent/Makefile
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
# CROSS_COMPILE=aarch64-linux-gnu- make
 | 
			
		||||
#
 | 
			
		||||
CC=gcc
 | 
			
		||||
 | 
			
		||||
ifdef DEBUG
 | 
			
		||||
	CFLAGS=-static -lc -g
 | 
			
		||||
else
 | 
			
		||||
	CFLAGS=-static -lc -O2
 | 
			
		||||
endif
 | 
			
		||||
 | 
			
		||||
revent: revent.c
 | 
			
		||||
	$(CROSS_COMPILE)$(CC) $(CFLAGS) revent.c -o revent
 | 
			
		||||
 | 
			
		||||
clean:
 | 
			
		||||
	rm -rf revent
 | 
			
		||||
 | 
			
		||||
.PHONY: clean
 | 
			
		||||
							
								
								
									
										1490
									
								
								wa/tools/revent/revent.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1490
									
								
								wa/tools/revent/revent.c
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										251
									
								
								wa/utils/revent.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										251
									
								
								wa/utils/revent.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,251 @@
 | 
			
		||||
#    Copyright 2016 ARM Limited
 | 
			
		||||
#
 | 
			
		||||
# Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
# you may not use this file except in compliance with the License.
 | 
			
		||||
# You may obtain a copy of the License at
 | 
			
		||||
#
 | 
			
		||||
#     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
#
 | 
			
		||||
# Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
# distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
# See the License for the specific language governing permissions and
 | 
			
		||||
# limitations under the License.
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
from __future__ import division
 | 
			
		||||
import os
 | 
			
		||||
import struct
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from collections import namedtuple
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
GENERAL_MODE = 0
 | 
			
		||||
GAMEPAD_MODE = 1
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
u16_struct = struct.Struct('<H')
 | 
			
		||||
u32_struct = struct.Struct('<I')
 | 
			
		||||
u64_struct = struct.Struct('<Q')
 | 
			
		||||
 | 
			
		||||
# See revent section in WA documentation for the detailed description of
 | 
			
		||||
# the recording format.
 | 
			
		||||
header_one_struct = struct.Struct('<6sH')
 | 
			
		||||
header_two_struct = struct.Struct('<H6x')  # version 2 onwards
 | 
			
		||||
 | 
			
		||||
devid_struct = struct.Struct('<4H')
 | 
			
		||||
devinfo_struct = struct.Struct('<4s96s96s96sI')
 | 
			
		||||
absinfo_struct = struct.Struct('<7i')
 | 
			
		||||
 | 
			
		||||
event_struct = struct.Struct('<HqqHHi')
 | 
			
		||||
old_event_struct = struct.Struct("<i4xqqHHi")  # prior to version 2
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def read_struct(fh, struct_spec):
 | 
			
		||||
    data = fh.read(struct_spec.size)
 | 
			
		||||
    return struct_spec.unpack(data)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def read_string(fh):
 | 
			
		||||
    length, = read_struct(fh, u32_struct)
 | 
			
		||||
    str_struct = struct.Struct('<{}s'.format(length))
 | 
			
		||||
    return read_struct(fh, str_struct)[0]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def count_bits(bitarr):
 | 
			
		||||
    return sum(bin(b).count('1') for b in bitarr)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def is_set(bitarr, bit):
 | 
			
		||||
    byte = bit // 8
 | 
			
		||||
    bytebit = bit % 8
 | 
			
		||||
    return bitarr[byte] & bytebit
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
absinfo = namedtuple('absinfo', 'ev_code value min max fuzz flat resolution')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UinputDeviceInfo(object):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, fh):
 | 
			
		||||
        parts = read_struct(fh, devid_struct)
 | 
			
		||||
        self.bustype = parts[0]
 | 
			
		||||
        self.vendor = parts[1]
 | 
			
		||||
        self.product = parts[2]
 | 
			
		||||
        self.version = parts[3]
 | 
			
		||||
 | 
			
		||||
        self.name = read_string(fh)
 | 
			
		||||
 | 
			
		||||
        parts = read_struct(fh, devinfo_struct)
 | 
			
		||||
        self.ev_bits = bytearray(parts[0])
 | 
			
		||||
        self.key_bits = bytearray(parts[1])
 | 
			
		||||
        self.rel_bits = bytearray(parts[2])
 | 
			
		||||
        self.abs_bits = bytearray(parts[3])
 | 
			
		||||
        self.num_absinfo = parts[4]
 | 
			
		||||
        self.absinfo = [absinfo(*read_struct(fh, absinfo_struct))
 | 
			
		||||
                        for _ in xrange(self.num_absinfo)]
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return 'UInputInfo({})'.format(self.__dict__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ReventEvent(object):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, fh, legacy=False):
 | 
			
		||||
        if not legacy:
 | 
			
		||||
            dev_id, ts_sec, ts_usec, type_, code, value = read_struct(fh, event_struct)
 | 
			
		||||
        else:
 | 
			
		||||
            dev_id, ts_sec, ts_usec, type_, code, value = read_struct(fh, old_event_struct)
 | 
			
		||||
        self.device_id = dev_id
 | 
			
		||||
        self.time = datetime.fromtimestamp(ts_sec + float(ts_usec) / 1000000)
 | 
			
		||||
        self.type = type_
 | 
			
		||||
        self.code = code
 | 
			
		||||
        self.value = value
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return 'InputEvent({})'.format(self.__dict__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ReventRecording(object):
 | 
			
		||||
    """
 | 
			
		||||
    Represents a parsed revent recording. This contains input events and device
 | 
			
		||||
    descriptions recorded by revent. Two parsing modes are supported. By
 | 
			
		||||
    default, the recording will be parsed in the "streaming" mode. In this
 | 
			
		||||
    mode, initial headers and device descritions are parsed on creation and an
 | 
			
		||||
    open file handle to the recording is saved. Events will be read from the
 | 
			
		||||
    file as they are being iterated over. In this mode, the entire recording is
 | 
			
		||||
    never loaded into memory at once. The underlying file may be "released" by
 | 
			
		||||
    calling ``close`` on the recroding, after which further iteration over the
 | 
			
		||||
    events will not be possible (but would still be possible to access the file
 | 
			
		||||
    description and header information).
 | 
			
		||||
 | 
			
		||||
    The alternative is to load the entire recording on creation (in which case
 | 
			
		||||
    the file handle will be closed once the recroding is loaded). This can be
 | 
			
		||||
    enabled by specifying ``streaming=False``. This will make it faster to
 | 
			
		||||
    subsequently iterate over the events, and also will not "hold" the file
 | 
			
		||||
    open.
 | 
			
		||||
 | 
			
		||||
    .. note:: When starting a new iteration over the events in streaming mode,
 | 
			
		||||
              the postion in the open file will be automatically reset to the
 | 
			
		||||
              beginning of the event stream. This means it's possible to iterate
 | 
			
		||||
              over the events multiple times without having to re-open the
 | 
			
		||||
              recording, however it is not possible to do so in parallel. If
 | 
			
		||||
              parallel iteration is required, streaming should be disabled.
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def duration(self):
 | 
			
		||||
        if self._duration is None:
 | 
			
		||||
            if self.stream:
 | 
			
		||||
                events = self._iter_events()
 | 
			
		||||
                try:
 | 
			
		||||
                    first = last = events.next()
 | 
			
		||||
                except StopIteration:
 | 
			
		||||
                    self._duration = 0
 | 
			
		||||
                for last in events:
 | 
			
		||||
                    pass
 | 
			
		||||
                self._duration = (last.time - first.time).total_seconds()
 | 
			
		||||
            else:  # not streaming
 | 
			
		||||
                if not self._events:
 | 
			
		||||
                    self._duration = 0
 | 
			
		||||
                self._duration = (self._events[-1].time -
 | 
			
		||||
                                  self._events[0].time).total_seconds()
 | 
			
		||||
        return self._duration
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def events(self):
 | 
			
		||||
        if self.stream:
 | 
			
		||||
            return self._iter_events()
 | 
			
		||||
        else:
 | 
			
		||||
            return self._events
 | 
			
		||||
 | 
			
		||||
    def __init__(self, f, stream=True):
 | 
			
		||||
        self.device_paths = []
 | 
			
		||||
        self.gamepad_device = None
 | 
			
		||||
        self.num_events = None
 | 
			
		||||
        self.stream = stream
 | 
			
		||||
        self._events = None
 | 
			
		||||
        self._close_when_done = False
 | 
			
		||||
        self._events_start = None
 | 
			
		||||
        self._duration = None
 | 
			
		||||
 | 
			
		||||
        if hasattr(f, 'name'):  # file-like object
 | 
			
		||||
            self.filepath = f.name
 | 
			
		||||
            self.fh = f
 | 
			
		||||
        else:  # path to file
 | 
			
		||||
            self.filepath = f
 | 
			
		||||
            self.fh = open(self.filepath, 'rb')
 | 
			
		||||
            if not self.stream:
 | 
			
		||||
                self._close_when_done = True
 | 
			
		||||
        try:
 | 
			
		||||
            self._parse_header_and_devices(self.fh)
 | 
			
		||||
            self._events_start = self.fh.tell()
 | 
			
		||||
            if not self.stream:
 | 
			
		||||
                self._events = [e for e in self._iter_events()]
 | 
			
		||||
        finally:
 | 
			
		||||
            if self._close_when_done:
 | 
			
		||||
                self.close()
 | 
			
		||||
 | 
			
		||||
    def close(self):
 | 
			
		||||
        if self.fh is not None:
 | 
			
		||||
            self.fh.close()
 | 
			
		||||
            self.fh = None
 | 
			
		||||
            self._events_start = None
 | 
			
		||||
 | 
			
		||||
    def _parse_header_and_devices(self, fh):
 | 
			
		||||
        magic, version = read_struct(fh, header_one_struct)
 | 
			
		||||
        if magic != 'REVENT':
 | 
			
		||||
            msg = '{} does not appear to be an revent recording'
 | 
			
		||||
            raise ValueError(msg.format(self.filepath))
 | 
			
		||||
        self.version = version
 | 
			
		||||
 | 
			
		||||
        if self.version == 2:
 | 
			
		||||
            self.mode, = read_struct(fh, header_two_struct)
 | 
			
		||||
            if self.mode == GENERAL_MODE:
 | 
			
		||||
                self._read_devices(fh)
 | 
			
		||||
            elif self.mode == GAMEPAD_MODE:
 | 
			
		||||
                self._read_gamepad_info(fh)
 | 
			
		||||
            else:
 | 
			
		||||
                raise ValueError('Unexpected recording mode: {}'.format(self.mode))
 | 
			
		||||
            self.num_events, = read_struct(fh, u64_struct)
 | 
			
		||||
        elif 2 > self.version >= 0:
 | 
			
		||||
            self.mode = GENERAL_MODE
 | 
			
		||||
            self._read_devices(fh)
 | 
			
		||||
        else:
 | 
			
		||||
            raise ValueError('Invalid recording version: {}'.format(self.version))
 | 
			
		||||
 | 
			
		||||
    def _read_devices(self, fh):
 | 
			
		||||
        num_devices, = read_struct(fh, u32_struct)
 | 
			
		||||
        for _ in xrange(num_devices):
 | 
			
		||||
            self.device_paths.append(read_string(fh))
 | 
			
		||||
 | 
			
		||||
    def _read_gamepad_info(self, fh):
 | 
			
		||||
        self.gamepad_device = UinputDeviceInfo(fh)
 | 
			
		||||
        self.device_paths.append('[GAMEPAD]')
 | 
			
		||||
 | 
			
		||||
    def _iter_events(self):
 | 
			
		||||
        if self.fh is None:
 | 
			
		||||
            msg = 'Attempting to iterate over events of a closed recording'
 | 
			
		||||
            raise RuntimeError(msg)
 | 
			
		||||
        self.fh.seek(self._events_start)
 | 
			
		||||
        if self.version >= 2:
 | 
			
		||||
            for _ in xrange(self.num_events):
 | 
			
		||||
                yield ReventEvent(self.fh)
 | 
			
		||||
        else:
 | 
			
		||||
            file_size = os.path.getsize(self.filepath)
 | 
			
		||||
            while self.fh.tell() < file_size:
 | 
			
		||||
                yield ReventEvent(self.fh, legacy=True)
 | 
			
		||||
 | 
			
		||||
    def __iter__(self):
 | 
			
		||||
        for event in self.events:
 | 
			
		||||
            yield event
 | 
			
		||||
 | 
			
		||||
    def __enter__(self):
 | 
			
		||||
        return self
 | 
			
		||||
 | 
			
		||||
    def __exit__(self, *args):
 | 
			
		||||
        self.close()
 | 
			
		||||
 | 
			
		||||
    def __del__(self):
 | 
			
		||||
        self.close()
 | 
			
		||||
		Reference in New Issue
	
	Block a user