From c38dc287ab79179560fa1c383b1ed73a567e9f1b Mon Sep 17 00:00:00 2001
From: jummp01 <jumana.mp@arm.com>
Date: Thu, 2 Feb 2017 17:31:20 +0000
Subject: [PATCH] Move UxperfParser into utils
UxperfParser class is moved from the UxperfResultProcessor class into a new
python module in utils. This will help to use the UxperfParser even when
the result procesor is not configured.
---
wlauto/result_processors/uxperf.py | 167 +---------------------------
wlauto/utils/uxperf.py | 170 +++++++++++++++++++++++++++++
2 files changed, 173 insertions(+), 164 deletions(-)
create mode 100644 wlauto/utils/uxperf.py
diff --git a/wlauto/result_processors/uxperf.py b/wlauto/result_processors/uxperf.py
index ff9027f8..6221afde 100755
--- a/wlauto/result_processors/uxperf.py
+++ b/wlauto/result_processors/uxperf.py
@@ -14,17 +14,13 @@
#
import os
-import re
-import logging
-
-from collections import defaultdict
from distutils.version import LooseVersion
+
from wlauto import ResultProcessor, Parameter
from wlauto.instrumentation import instrument_is_enabled
-from wlauto.instrumentation.fps import VSYNC_INTERVAL
from wlauto.exceptions import ResultProcessorError, ConfigError
-from wlauto.utils.fps import FpsProcessor, SurfaceFlingerFrame, GfxInfoFrame
from wlauto.utils.types import numeric, boolean
+from wlauto.utils.uxperf import UxPerfParser
try:
import pandas as pd
@@ -77,6 +73,7 @@ class UxPerfResultProcessor(ResultProcessor):
]
def initialize(self, context):
+ # needed for uxperf parser
if not pd or LooseVersion(pd.__version__) < LooseVersion('0.13.1'):
message = ('uxperf result processor requires pandas Python package '
'(version 0.13.1 or higher) to be installed.\n'
@@ -101,161 +98,3 @@ class UxPerfResultProcessor(ResultProcessor):
if self.add_frames:
self.logger.debug('Adding per-action frame metrics')
parser.add_action_frames(framelog, self.drop_threshold, self.generate_csv)
-
-
-class UxPerfParser(object):
- '''
- Parses logcat messages for UX Performance markers.
-
- UX Performance markers are output from logcat under a debug priority. The
- logcat tag for the marker messages is UX_PERF. The messages associated with
- this tag consist of a name for the action to be recorded and a timestamp.
- These fields are delimited by a single space. e.g.
-
- <TAG> : <MESSAGE>
- UX_PERF : gestures_swipe_left_start 861975087367
- ...
- ...
- UX_PERF : gestures_swipe_left_end 862132085804
-
- Timestamps are produced using the running Java Virtual Machine's
- high-resolution time source, in nanoseconds.
- '''
- def __init__(self, context):
- self.context = context
- self.actions = defaultdict(list)
- self.logger = logging.getLogger('UxPerfParser')
- # regex for matching logcat message format:
- self.regex = re.compile(r'UX_PERF.*?:\s*(?P<message>.*\d+$)')
-
- def parse(self, log):
- '''
- Opens log file and parses UX_PERF markers.
-
- Actions delimited by markers are captured in a dictionary with
- actions mapped to timestamps.
- '''
- loglines = self._read(log)
- self._gen_action_timestamps(loglines)
-
- def add_action_frames(self, frames, drop_threshold, generate_csv): # pylint: disable=too-many-locals
- '''
- Uses FpsProcessor to parse frame.csv extracting fps, frame count, jank
- and vsync metrics on a per action basis. Adds results to metrics.
- '''
- refresh_period = self._parse_refresh_peroid()
-
- for action in self.actions:
- # default values
- fps, frame_count, janks, not_at_vsync = float('nan'), 0, 0, 0
- p90, p95, p99 = [float('nan')] * 3
- metrics = (fps, frame_count, janks, not_at_vsync)
-
- df = self._create_sub_df(self.actions[action], frames)
- if not df.empty: # pylint: disable=maybe-no-member
- fp = FpsProcessor(df, action=action)
- try:
- per_frame_fps, metrics = fp.process(refresh_period, drop_threshold)
- fps, frame_count, janks, not_at_vsync = metrics
-
- if generate_csv:
- name = action + '_fps'
- filename = name + '.csv'
- fps_outfile = os.path.join(self.context.output_directory, filename)
- per_frame_fps.to_csv(fps_outfile, index=False, header=True)
- self.context.add_artifact(name, path=filename, kind='data')
-
- p90, p95, p99 = fp.percentiles()
- except AttributeError:
- self.logger.warning('Non-matched timestamps in dumpsys output: action={}'
- .format(action))
-
- self.context.result.add_metric(action + '_FPS', fps)
- self.context.result.add_metric(action + '_frame_count', frame_count)
- self.context.result.add_metric(action + '_janks', janks, lower_is_better=True)
- self.context.result.add_metric(action + '_not_at_vsync', not_at_vsync, lower_is_better=True)
- self.context.result.add_metric(action + '_frame_time_90percentile', p90, 'ms', lower_is_better=True)
- self.context.result.add_metric(action + '_frame_time_95percentile', p95, 'ms', lower_is_better=True)
- self.context.result.add_metric(action + '_frame_time_99percentile', p99, 'ms', lower_is_better=True)
-
- def add_action_timings(self):
- '''
- Add simple action timings in millisecond resolution to metrics
- '''
- for action, timestamps in self.actions.iteritems():
- # nanosecond precision, but not necessarily nanosecond resolution
- # truncate to guarantee millisecond precision
- ts_ms = tuple(int(ts) for ts in timestamps)
- if len(ts_ms) == 2:
- start, finish = ts_ms
- duration = finish - start
- result = self.context.result
-
- result.add_metric(action + "_start", start, units='ms')
- result.add_metric(action + "_finish", finish, units='ms')
- result.add_metric(action + "_duration", duration, units='ms', lower_is_better=True)
- else:
- self.logger.warning('Expected two timestamps. Received {}'.format(ts_ms))
-
- def _gen_action_timestamps(self, lines):
- '''
- Parses lines and matches against logcat tag.
- Groups timestamps by action name.
- Creates a dictionary of lists with actions mapped to timestamps.
- '''
- for line in lines:
- match = self.regex.search(line)
-
- if match:
- message = match.group('message')
- action_with_suffix, timestamp = message.rsplit(' ', 1)
- action, _ = action_with_suffix.rsplit('_', 1)
- self.actions[action].append(timestamp)
-
- def _parse_refresh_peroid(self):
- '''
- Reads the first line of the raw dumpsys output for the refresh period.
- '''
- raw_path = os.path.join(self.context.output_directory, 'surfaceflinger.raw')
- if os.path.isfile(raw_path):
- raw_lines = self._read(raw_path)
- refresh_period = int(raw_lines.next())
- else:
- refresh_period = VSYNC_INTERVAL
-
- return refresh_period
-
- def _create_sub_df(self, action, frames):
- '''
- Creates a data frame containing fps metrics for a captured action.
- '''
- if len(action) == 2:
- start, end = map(int, action)
- df = pd.read_csv(frames)
- # SurfaceFlinger Algorithm
- if df.columns.tolist() == list(SurfaceFlingerFrame._fields): # pylint: disable=maybe-no-member
- field = 'actual_present_time'
- # GfxInfo Algorithm
- elif df.columns.tolist() == list(GfxInfoFrame._fields): # pylint: disable=maybe-no-member
- field = 'FrameCompleted'
- else:
- field = ''
- self.logger.error('frames.csv not in a recognised format. Cannot parse.')
- if field:
- df = df[start < df[field]]
- df = df[df[field] <= end]
- else:
- self.logger.warning('Discarding action. Expected 2 timestamps, got {}!'.format(len(action)))
- df = pd.DataFrame()
- return df
-
- def _read(self, log):
- '''
- Opens a file a yields the lines with whitespace stripped.
- '''
- try:
- with open(log, 'r') as rfh:
- for line in rfh:
- yield line.strip()
- except IOError:
- self.logger.error('Could not open {}'.format(log))
diff --git a/wlauto/utils/uxperf.py b/wlauto/utils/uxperf.py
new file mode 100644
index 00000000..36b9ee06
--- /dev/null
+++ b/wlauto/utils/uxperf.py
@@ -0,0 +1,170 @@
+import os
+import re
+import logging
+from collections import defaultdict
+
+from wlauto.utils.fps import FpsProcessor, SurfaceFlingerFrame, GfxInfoFrame, VSYNC_INTERVAL
+
+try:
+ import pandas as pd
+except ImportError:
+ pd = None
+
+
+class UxPerfParser(object):
+ '''
+ Parses logcat messages for UX Performance markers.
+
+ UX Performance markers are output from logcat under a debug priority. The
+ logcat tag for the marker messages is UX_PERF. The messages associated with
+ this tag consist of a name for the action to be recorded and a timestamp.
+ These fields are delimited by a single space. e.g.
+
+ <TAG> : <MESSAGE>
+ UX_PERF : gestures_swipe_left_start 861975087367
+ ...
+ ...
+ UX_PERF : gestures_swipe_left_end 862132085804
+
+ Timestamps are produced using the running Java Virtual Machine's
+ high-resolution time source, in nanoseconds.
+ '''
+ def __init__(self, context, prefix=''):
+ self.context = context
+ self.prefix = prefix
+ self.actions = defaultdict(list)
+ self.logger = logging.getLogger('UxPerfParser')
+ # regex for matching logcat message format:
+ self.regex = re.compile(r'UX_PERF.*?:\s*(?P<message>.*\d+$)')
+
+ def parse(self, log):
+ '''
+ Opens log file and parses UX_PERF markers.
+
+ Actions delimited by markers are captured in a dictionary with
+ actions mapped to timestamps.
+ '''
+ loglines = self._read(log)
+ self._gen_action_timestamps(loglines)
+
+ def add_action_frames(self, frames, drop_threshold, generate_csv): # pylint: disable=too-many-locals
+ '''
+ Uses FpsProcessor to parse frame.csv extracting fps, frame count, jank
+ and vsync metrics on a per action basis. Adds results to metrics.
+ '''
+ refresh_period = self._parse_refresh_peroid()
+
+ for action in self.actions:
+ # default values
+ fps, frame_count, janks, not_at_vsync = float('nan'), 0, 0, 0
+ p90, p95, p99 = [float('nan')] * 3
+ metrics = (fps, frame_count, janks, not_at_vsync)
+
+ df = self._create_sub_df(self.actions[action], frames)
+ if not df.empty: # pylint: disable=maybe-no-member
+ fp = FpsProcessor(df, action=action)
+ try:
+ per_frame_fps, metrics = fp.process(refresh_period, drop_threshold)
+ fps, frame_count, janks, not_at_vsync = metrics
+
+ if generate_csv:
+ name = action + '_fps'
+ filename = name + '.csv'
+ fps_outfile = os.path.join(self.context.output_directory, filename)
+ per_frame_fps.to_csv(fps_outfile, index=False, header=True)
+ self.context.add_artifact(name, path=filename, kind='data')
+
+ p90, p95, p99 = fp.percentiles()
+ except AttributeError:
+ self.logger.warning('Non-matched timestamps in dumpsys output: action={}'
+ .format(action))
+
+ self.context.result.add_metric(self.prefix + action + '_FPS', fps)
+ self.context.result.add_metric(self.prefix + action + '_frame_count', frame_count)
+ self.context.result.add_metric(self.prefix + action + '_janks', janks, lower_is_better=True)
+ self.context.result.add_metric(self.prefix + action + '_not_at_vsync', not_at_vsync, lower_is_better=True)
+ self.context.result.add_metric(self.prefix + action + '_frame_time_90percentile', p90, 'ms', lower_is_better=True)
+ self.context.result.add_metric(self.prefix + action + '_frame_time_95percentile', p95, 'ms', lower_is_better=True)
+ self.context.result.add_metric(self.prefix + action + '_frame_time_99percentile', p99, 'ms', lower_is_better=True)
+
+ def add_action_timings(self):
+ '''
+ Add simple action timings in millisecond resolution to metrics
+ '''
+ for action, timestamps in self.actions.iteritems():
+ # nanosecond precision, but not necessarily nanosecond resolution
+ # truncate to guarantee millisecond precision
+ ts_ms = tuple(int(ts) for ts in timestamps)
+ if len(ts_ms) == 2:
+ start, finish = ts_ms
+ duration = finish - start
+ result = self.context.result
+
+ result.add_metric(self.prefix + action + "_start", start, units='ms')
+ result.add_metric(self.prefix + action + "_finish", finish, units='ms')
+ result.add_metric(self.prefix + action + "_duration", duration, units='ms', lower_is_better=True)
+ else:
+ self.logger.warning('Expected two timestamps. Received {}'.format(ts_ms))
+
+ def _gen_action_timestamps(self, lines):
+ '''
+ Parses lines and matches against logcat tag.
+ Groups timestamps by action name.
+ Creates a dictionary of lists with actions mapped to timestamps.
+ '''
+ for line in lines:
+ match = self.regex.search(line)
+
+ if match:
+ message = match.group('message')
+ action_with_suffix, timestamp = message.rsplit(' ', 1)
+ action, _ = action_with_suffix.rsplit('_', 1)
+ self.actions[action].append(timestamp)
+
+ def _parse_refresh_peroid(self):
+ '''
+ Reads the first line of the raw dumpsys output for the refresh period.
+ '''
+ raw_path = os.path.join(self.context.output_directory, 'surfaceflinger.raw')
+ if os.path.isfile(raw_path):
+ raw_lines = self._read(raw_path)
+ refresh_period = int(raw_lines.next())
+ else:
+ refresh_period = VSYNC_INTERVAL
+
+ return refresh_period
+
+ def _create_sub_df(self, action, frames):
+ '''
+ Creates a data frame containing fps metrics for a captured action.
+ '''
+ if len(action) == 2:
+ start, end = map(int, action)
+ df = pd.read_csv(frames)
+ # SurfaceFlinger Algorithm
+ if df.columns.tolist() == list(SurfaceFlingerFrame._fields): # pylint: disable=maybe-no-member
+ field = 'actual_present_time'
+ # GfxInfo Algorithm
+ elif df.columns.tolist() == list(GfxInfoFrame._fields): # pylint: disable=maybe-no-member
+ field = 'FrameCompleted'
+ else:
+ field = ''
+ self.logger.error('frames.csv not in a recognised format. Cannot parse.')
+ if field:
+ df = df[start < df[field]]
+ df = df[df[field] <= end]
+ else:
+ self.logger.warning('Discarding action. Expected 2 timestamps, got {}!'.format(len(action)))
+ df = pd.DataFrame()
+ return df
+
+ def _read(self, log):
+ '''
+ Opens a file a yields the lines with whitespace stripped.
+ '''
+ try:
+ with open(log, 'r') as rfh:
+ for line in rfh:
+ yield line.strip()
+ except IOError:
+ self.logger.error('Could not open {}'.format(log))