derived: add DerivedGfxInfoStats
Add DerivedGfxInfoStats that parse output from GfxInfoFramesInstrument
to produce FPS data and rendering statistics.
diff --git a/devlib/__init__.py b/devlib/__init__.py
index 42509f9..efcf0bc 100644
--- a/devlib/__init__.py
+++ b/devlib/__init__.py
@@ -21,6 +21,7 @@
from devlib.derived import DerivedMeasurements, DerivedMetric
from devlib.derived.energy import DerivedEnergyMeasurements
+from devlib.derived.fps import DerivedGfxInfoStats
from devlib.trace.ftrace import FtraceCollector
diff --git a/devlib/derived/fps.py b/devlib/derived/fps.py
new file mode 100644
index 0000000..31a5399
--- /dev/null
+++ b/devlib/derived/fps.py
@@ -0,0 +1,137 @@
+from __future__ import division
+import csv
+import os
+import re
+
+try:
+ import pandas as pd
+except ImportError:
+ pd = None
+
+from devlib import DerivedMeasurements, DerivedMetric, MeasurementsCsv, InstrumentChannel
+from devlib.utils.rendering import gfxinfo_get_last_dump, VSYNC_INTERVAL
+from devlib.utils.types import numeric
+
+
+class DerivedFpsStats(DerivedMeasurements):
+
+ def __init__(self, drop_threshold=5, suffix=None, filename=None, outdir=None):
+ self.drop_threshold = drop_threshold
+ self.suffix = suffix
+ self.filename = filename
+ self.outdir = outdir
+ if (filename is None) and (suffix is None):
+ self.suffix = '-fps'
+ elif (filename is not None) and (suffix is not None):
+ raise ValueError('suffix and filename cannot be specified at the same time.')
+ if filename is not None and os.sep in filename:
+ raise ValueError('filename cannot be a path (cannot countain "{}"'.format(os.sep))
+
+ def process(self, measurements_csv):
+ if isinstance(measurements_csv, basestring):
+ measurements_csv = MeasurementsCsv(measurements_csv)
+ if pd is not None:
+ return self._process_with_pandas(measurements_csv)
+ return self._process_without_pandas(measurements_csv)
+
+ def _get_csv_file_name(self, frames_file):
+ outdir = self.outdir or os.path.dirname(frames_file)
+ if self.filename:
+ return os.path.join(outdir, self.filename)
+
+ frames_basename = os.path.basename(frames_file)
+ rest, ext = os.path.splitext(frames_basename)
+ csv_basename = rest + self.suffix + ext
+ return os.path.join(outdir, csv_basename)
+
+
+class DerivedGfxInfoStats(DerivedFpsStats):
+
+ @staticmethod
+ def process_raw(filepath, *args):
+ metrics = []
+ dump = gfxinfo_get_last_dump(filepath)
+ seen_stats = False
+ for line in dump.split('\n'):
+ if seen_stats and not line.strip():
+ break
+ elif line.startswith('Janky frames:'):
+ text = line.split(': ')[-1]
+ val_text, pc_text = text.split('(')
+ metrics.append(DerivedMetric('janks', numeric(val_text.strip()), 'count'))
+ metrics.append(DerivedMetric('janks_pc', numeric(pc_text[:-3]), 'percent'))
+ elif ' percentile: ' in line:
+ ptile, val_text = line.split(' percentile: ')
+ name = 'render_time_{}_ptile'.format(ptile)
+ value = numeric(val_text.strip()[:-2])
+ metrics.append(DerivedMetric(name, value, 'time_ms'))
+ elif line.startswith('Number '):
+ name_text, val_text = line.strip().split(': ')
+ name = name_text[7:].lower().replace(' ', '_')
+ value = numeric(val_text)
+ metrics.append(DerivedMetric(name, value, 'count'))
+ else:
+ continue
+ seen_stats = True
+ return metrics
+
+ def _process_without_pandas(self, measurements_csv):
+ per_frame_fps = []
+ start_vsync, end_vsync = None, None
+ frame_count = 0
+
+ for frame_data in measurements_csv.iter_values():
+ if frame_data.Flags_flags != 0:
+ continue
+ frame_count += 1
+
+ if start_vsync is None:
+ start_vsync = frame_data.Vsync_time_us
+ end_vsync = frame_data.Vsync_time_us
+
+ frame_time = frame_data.FrameCompleted_time_us - frame_data.IntendedVsync_time_us
+ pff = 1e9 / frame_time
+ if pff > self.drop_threshold:
+ per_frame_fps.append([pff])
+
+ if frame_count:
+ duration = end_vsync - start_vsync
+ fps = (1e9 * frame_count) / float(duration)
+ else:
+ duration = 0
+ fps = 0
+
+ csv_file = self._get_csv_file_name(measurements_csv.path)
+ with open(csv_file, 'wb') as wfh:
+ writer = csv.writer(wfh)
+ writer.writerow(['fps'])
+ writer.writerows(per_frame_fps)
+
+ return [DerivedMetric('fps', fps, 'fps'),
+ DerivedMetric('total_frames', frame_count, 'frames'),
+ MeasurementsCsv(csv_file)]
+
+ def _process_with_pandas(self, measurements_csv):
+ data = pd.read_csv(measurements_csv.path)
+ data = data[data.Flags_flags == 0]
+ frame_time = data.FrameCompleted_time_us - data.IntendedVsync_time_us
+ per_frame_fps = (1e9 / frame_time)
+ keep_filter = per_frame_fps > self.drop_threshold
+ per_frame_fps = per_frame_fps[keep_filter]
+ per_frame_fps.name = 'fps'
+
+ frame_count = data.index.size
+ if frame_count > 1:
+ duration = data.Vsync_time_us.iloc[-1] - data.Vsync_time_us.iloc[0]
+ fps = (1e9 * frame_count) / float(duration)
+ else:
+ duration = 0
+ fps = 0
+
+ csv_file = self._get_csv_file_name(measurements_csv.path)
+ per_frame_fps.to_csv(csv_file, index=False, header=True)
+
+ return [DerivedMetric('fps', fps, 'fps'),
+ DerivedMetric('total_frames', frame_count, 'frames'),
+ MeasurementsCsv(csv_file)]
+
diff --git a/devlib/utils/rendering.py b/devlib/utils/rendering.py
index 665135a..7bb6f3a 100644
--- a/devlib/utils/rendering.py
+++ b/devlib/utils/rendering.py
@@ -18,6 +18,8 @@
SurfaceFlingerFrame = namedtuple('SurfaceFlingerFrame',
'desired_present_time actual_present_time frame_ready_time')
+VSYNC_INTERVAL = 16666667
+
class FrameCollector(threading.Thread):
diff --git a/doc/derived_measurements.rst b/doc/derived_measurements.rst
index 285bce6..0a678bb 100644
--- a/doc/derived_measurements.rst
+++ b/doc/derived_measurements.rst
@@ -124,3 +124,58 @@
sites of the coresponding channels according to the following patters:
``"<site>_total_energy"`` and ``"<site>_average_power"``.
+
+FPS / Rendering
+~~~~~~~~~~~~~~~
+
+.. class:: DerivedGfxInfoStats(drop_threshold=5, suffix='-fps', filename=None, outdir=None)
+
+ Produces FPS (frames-per-second) and other dervied statistics from
+ :class:`GfxInfoFramesInstrument` output. This takes several optional
+ parameters in creation:
+
+ :param drop_threshold: FPS in an application, such as a game, which this
+ processor is primarily targeted at, cannot reasonably
+ drop to a very low value. This is specified to this
+ threhold. If an FPS for a frame is computed to be
+ lower than this treshold, it will be dropped on the
+ assumption that frame rednering was suspended by the
+ system (e.g. when idling), or there was some sort of
+ error, and therefore this should be used in
+ performance calculations. defaults to ``5``.
+ :param suffix: The name of the gerated per-frame FPS csv file will be
+ derived from the input frames csv file by appending this
+ suffix. This cannot be specified at the same time as
+ a ``filename``.
+ :param filename: As an alternative to the suffix, a complete file name for
+ FPS csv can be specified. This cannot be used at the same
+ time as the ``suffix``.
+ :param outdir: By default, the FPS csv file will be placed in the same
+ directory as the input frames csv file. This can be changed
+ by specifying an alternate directory here
+
+ .. warning:: Specifying both ``filename`` and ``oudir`` will mean that exactly
+ the same file will be used for FPS output on each invocation of
+ ``process()`` (even for different inputs) resulting in previous
+ results being overwritten.
+
+.. method:: DerivedGfxInfoStats.process(measurement_csv)
+
+ Process the fames csv generated by :class:`GfxInfoFramesInstrument` and
+ returns a list containing exactly three entries: :class:`DerivedMetric`\ s
+ ``fps`` and ``total_frames``, followed by a :class:`MeasurentCsv` containing
+ per-frame FPSs values.
+
+.. method:: DerivedGfxInfoStats.process_raw(gfxinfo_frame_raw_file)
+
+ As input, this takes a single argument, which should be the path to the raw
+ output file of :class:`GfxInfoFramesInstrument`. The returns stats
+ accumulated by gfxinfo. At the time of wrinting, the stats (in order) are:
+ ``janks``, ``janks_pc`` (percentage of all frames),
+ ``render_time_50th_ptile`` (50th percentile, or median, for time to render a
+ frame), ``render_time_90th_ptile``, ``render_time_95th_ptile``,
+ ``render_time_99th_ptile``, ``missed_vsync``, ``hight_input_latency``,
+ ``slow_ui_thread``, ``slow_bitmap_uploads``, ``slow_issue_draw_commands``.
+ Please see the `gfxinfo documentation`_ for details.
+
+.. _gfxinfo documentation: https://developer.android.com/training/testing/performance.html