1
0
mirror of https://github.com/ARM-software/devlib.git synced 2025-04-07 18:30:03 +01:00
devlib/devlib/utils/rendering.py
Sergei Trofimov 9fd690efb3 Update copyrights
- Update the year in the copyrights to match the last year the file was
  modified.
- Add the copyright header to files that did not already have one.
2018-07-04 16:01:47 +01:00

273 lines
9.1 KiB
Python

# Copyright 2018 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 logging
import os
import re
import shutil
import sys
import tempfile
import threading
import time
from collections import namedtuple, OrderedDict
from distutils.version import LooseVersion
from devlib.exception import WorkerThreadError, TargetNotRespondingError, TimeoutError
from devlib.utils.csvutil import csvwriter
logger = logging.getLogger('rendering')
SurfaceFlingerFrame = namedtuple('SurfaceFlingerFrame',
'desired_present_time actual_present_time frame_ready_time')
VSYNC_INTERVAL = 16666667
class FrameCollector(threading.Thread):
def __init__(self, target, period):
super(FrameCollector, self).__init__()
self.target = target
self.period = period
self.stop_signal = threading.Event()
self.frames = []
self.temp_file = None
self.refresh_period = None
self.drop_threshold = None
self.unresponsive_count = 0
self.last_ready_time = None
self.exc = None
self.header = None
def run(self):
logger.debug('Surface flinger frame data collection started.')
try:
self.stop_signal.clear()
fd, self.temp_file = tempfile.mkstemp()
logger.debug('temp file: {}'.format(self.temp_file))
wfh = os.fdopen(fd, 'wb')
try:
while not self.stop_signal.is_set():
self.collect_frames(wfh)
time.sleep(self.period)
finally:
wfh.close()
except (TargetNotRespondingError, TimeoutError): # pylint: disable=W0703
raise
except Exception as e: # pylint: disable=W0703
logger.warning('Exception on collector thread: {}({})'.format(e.__class__.__name__, e))
self.exc = WorkerThreadError(self.name, sys.exc_info())
logger.debug('Surface flinger frame data collection stopped.')
def stop(self):
self.stop_signal.set()
self.join()
if self.unresponsive_count:
message = 'FrameCollector was unrepsonsive {} times.'.format(self.unresponsive_count)
if self.unresponsive_count > 10:
logger.warning(message)
else:
logger.debug(message)
if self.exc:
raise self.exc # pylint: disable=E0702
def process_frames(self, outfile=None):
if not self.temp_file:
raise RuntimeError('Attempting to process frames before running the collector')
with open(self.temp_file) as fh:
self._process_raw_file(fh)
if outfile:
shutil.copy(self.temp_file, outfile)
os.unlink(self.temp_file)
self.temp_file = None
def write_frames(self, outfile, columns=None):
if columns is None:
header = self.header
frames = self.frames
else:
indexes = []
for c in columns:
if c not in self.header:
msg = 'Invalid column "{}"; must be in {}'
raise ValueError(msg.format(c, self.header))
indexes.append(self.header.index(c))
frames = [[f[i] for i in indexes] for f in self.frames]
header = columns
with csvwriter(outfile) as writer:
if header:
writer.writerow(header)
writer.writerows(frames)
def collect_frames(self, wfh):
raise NotImplementedError()
def clear(self):
raise NotImplementedError()
def _process_raw_file(self, fh):
raise NotImplementedError()
class SurfaceFlingerFrameCollector(FrameCollector):
def __init__(self, target, period, view, header=None):
super(SurfaceFlingerFrameCollector, self).__init__(target, period)
self.view = view
self.header = header or SurfaceFlingerFrame._fields
def collect_frames(self, wfh):
for activity in self.list():
if activity == self.view:
wfh.write(self.get_latencies(activity))
def clear(self):
self.target.execute('dumpsys SurfaceFlinger --latency-clear ')
def get_latencies(self, activity):
cmd = 'dumpsys SurfaceFlinger --latency "{}"'
return self.target.execute(cmd.format(activity))
def list(self):
text = self.target.execute('dumpsys SurfaceFlinger --list')
return text.replace('\r\n', '\n').replace('\r', '\n').split('\n')
def _process_raw_file(self, fh):
text = fh.read().replace('\r\n', '\n').replace('\r', '\n')
for line in text.split('\n'):
line = line.strip()
if line:
self._process_trace_line(line)
def _process_trace_line(self, line):
parts = line.split()
if len(parts) == 3:
frame = SurfaceFlingerFrame(*list(map(int, parts)))
if not frame.frame_ready_time:
return # "null" frame
if frame.frame_ready_time <= self.last_ready_time:
return # duplicate frame
if (frame.frame_ready_time - frame.desired_present_time) > self.drop_threshold:
logger.debug('Dropping bogus frame {}.'.format(line))
return # bogus data
self.last_ready_time = frame.frame_ready_time
self.frames.append(frame)
elif len(parts) == 1:
self.refresh_period = int(parts[0])
self.drop_threshold = self.refresh_period * 1000
elif 'SurfaceFlinger appears to be unresponsive, dumping anyways' in line:
self.unresponsive_count += 1
else:
logger.warning('Unexpected SurfaceFlinger dump output: {}'.format(line))
def read_gfxinfo_columns(target):
output = target.execute('dumpsys gfxinfo --list framestats')
lines = iter(output.split('\n'))
for line in lines:
if line.startswith('---PROFILEDATA---'):
break
columns_line = next(lines)
return columns_line.split(',')[:-1] # has a trailing ','
class GfxinfoFrameCollector(FrameCollector):
def __init__(self, target, period, package, header=None):
super(GfxinfoFrameCollector, self).__init__(target, period)
self.package = package
self.header = None
self._init_header(header)
def collect_frames(self, wfh):
cmd = 'dumpsys gfxinfo {} framestats'
wfh.write(self.target.execute(cmd.format(self.package)))
def clear(self):
pass
def _init_header(self, header):
if header is not None:
self.header = header
else:
self.header = read_gfxinfo_columns(self.target)
def _process_raw_file(self, fh):
found = False
try:
last_vsync = 0
while True:
for line in fh:
if line.startswith('---PROFILEDATA---'):
found = True
break
next(fh) # headers
for line in fh:
if line.startswith('---PROFILEDATA---'):
break
entries = list(map(int, line.strip().split(',')[:-1])) # has a trailing ','
if entries[1] <= last_vsync:
continue # repeat frame
last_vsync = entries[1]
self.frames.append(entries)
except StopIteration:
pass
if not found:
logger.warning('Could not find frames data in gfxinfo output')
return
def _file_reverse_iter(fh, buf_size=1024):
fh.seek(0, os.SEEK_END)
offset = 0
file_size = remaining_size = fh.tell()
while remaining_size > 0:
offset = min(file_size, offset + buf_size)
fh.seek(file_size - offset)
buf = fh.read(min(remaining_size, buf_size))
remaining_size -= buf_size
yield buf
def gfxinfo_get_last_dump(filepath):
"""
Return the last gfxinfo dump from the frame collector's raw output.
"""
record = ''
with open(filepath, 'r') as fh:
fh_iter = _file_reverse_iter(fh)
try:
while True:
buf = next(fh_iter)
ix = buf.find('** Graphics')
if ix >= 0:
return buf[ix:] + record
ix = buf.find(' **\n')
if ix >= 0:
buf = next(fh_iter) + buf
ix = buf.find('** Graphics')
if ix < 0:
msg = '"{}" appears to be corrupted'
raise RuntimeError(msg.format(filepath))
return buf[ix:] + record
record = buf + record
except StopIteration:
pass