mirror of
https://github.com/ARM-software/workload-automation.git
synced 2025-01-18 12:06:08 +00:00
revent workload part 1
This commit is contained in:
parent
b8361f8e09
commit
bddabbc56d
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()
|
Loading…
x
Reference in New Issue
Block a user