derived: add DerivedSurfaceFlingerStats
Add DerivedSurfaceFlingerStats that parse output from
SurfaceFlingerFramesInstrument to produce FPS data and rendering
statistics.
diff --git a/devlib/__init__.py b/devlib/__init__.py
index efcf0bc..2b3f3b6 100644
--- a/devlib/__init__.py
+++ b/devlib/__init__.py
@@ -13,7 +13,7 @@
from devlib.instrument import MEASUREMENT_TYPES, INSTANTANEOUS, CONTINUOUS
from devlib.instrument.daq import DaqInstrument
from devlib.instrument.energy_probe import EnergyProbeInstrument
-from devlib.instrument.frames import GfxInfoFramesInstrument
+from devlib.instrument.frames import GfxInfoFramesInstrument, SurfaceFlingerFramesInstrument
from devlib.instrument.hwmon import HwmonInstrument
from devlib.instrument.monsoon import MonsoonInstrument
from devlib.instrument.netstats import NetstatsInstrument
@@ -21,7 +21,7 @@
from devlib.derived import DerivedMeasurements, DerivedMetric
from devlib.derived.energy import DerivedEnergyMeasurements
-from devlib.derived.fps import DerivedGfxInfoStats
+from devlib.derived.fps import DerivedGfxInfoStats, DerivedSurfaceFlingerStats
from devlib.trace.ftrace import FtraceCollector
diff --git a/devlib/derived/fps.py b/devlib/derived/fps.py
index 31a5399..e595926 100644
--- a/devlib/derived/fps.py
+++ b/devlib/derived/fps.py
@@ -9,6 +9,7 @@
pd = None
from devlib import DerivedMeasurements, DerivedMetric, MeasurementsCsv, InstrumentChannel
+from devlib.exception import HostError
from devlib.utils.rendering import gfxinfo_get_last_dump, VSYNC_INTERVAL
from devlib.utils.types import numeric
@@ -135,3 +136,79 @@
DerivedMetric('total_frames', frame_count, 'frames'),
MeasurementsCsv(csv_file)]
+
+class DerivedSurfaceFlingerStats(DerivedFpsStats):
+
+ def _process_with_pandas(self, measurements_csv):
+ data = pd.read_csv(measurements_csv.path)
+
+ # fiter out bogus frames.
+ bogus_frames_filter = data.actual_present_time_us != 0x7fffffffffffffff
+ actual_present_times = data.actual_present_time_us[bogus_frames_filter]
+ actual_present_time_deltas = actual_present_times.diff().dropna()
+
+ vsyncs_to_compose = actual_present_time_deltas.div(VSYNC_INTERVAL)
+ vsyncs_to_compose.apply(lambda x: int(round(x, 0)))
+
+ # drop values lower than drop_threshold FPS as real in-game frame
+ # rate is unlikely to drop below that (except on loading screens
+ # etc, which should not be factored in frame rate calculation).
+ per_frame_fps = (1.0 / (vsyncs_to_compose.multiply(VSYNC_INTERVAL / 1e9)))
+ keep_filter = per_frame_fps > self.drop_threshold
+ filtered_vsyncs_to_compose = vsyncs_to_compose[keep_filter]
+ per_frame_fps.name = 'fps'
+
+ csv_file = self._get_csv_file_name(measurements_csv.path)
+ per_frame_fps.to_csv(csv_file, index=False, header=True)
+
+ if not filtered_vsyncs_to_compose.empty:
+ fps = 0
+ total_vsyncs = filtered_vsyncs_to_compose.sum()
+ frame_count = filtered_vsyncs_to_compose.size
+
+ if total_vsyncs:
+ fps = 1e9 * frame_count / (VSYNC_INTERVAL * total_vsyncs)
+
+ janks = self._calc_janks(filtered_vsyncs_to_compose)
+ not_at_vsync = self._calc_not_at_vsync(vsyncs_to_compose)
+ else:
+ fps = 0
+ frame_count = 0
+ janks = 0
+ not_at_vsync = 0
+
+ return [DerivedMetric('fps', fps, 'fps'),
+ DerivedMetric('total_frames', frame_count, 'frames'),
+ MeasurementsCsv(csv_file),
+ DerivedMetric('janks', janks, 'count'),
+ DerivedMetric('janks_pc', janks * 100 / frame_count, 'percent'),
+ DerivedMetric('missed_vsync', not_at_vsync, 'count')]
+
+ def _process_without_pandas(self, measurements_csv):
+ # Given that SurfaceFlinger has been deprecated in favor of GfxInfo,
+ # it does not seem worth it implementing this.
+ raise HostError('Please install "pandas" Python package to process SurfaceFlinger frames')
+
+ @staticmethod
+ def _calc_janks(filtered_vsyncs_to_compose):
+ """
+ Internal method for calculating jank frames.
+ """
+ pause_latency = 20
+ vtc_deltas = filtered_vsyncs_to_compose.diff().dropna()
+ vtc_deltas = vtc_deltas.abs()
+ janks = vtc_deltas.apply(lambda x: (pause_latency > x > 1.5) and 1 or 0).sum()
+
+ return janks
+
+ @staticmethod
+ def _calc_not_at_vsync(vsyncs_to_compose):
+ """
+ Internal method for calculating the number of frames that did not
+ render in a single vsync cycle.
+ """
+ epsilon = 0.0001
+ func = lambda x: (abs(x - 1.0) > epsilon) and 1 or 0
+ not_at_vsync = vsyncs_to_compose.apply(func).sum()
+
+ return not_at_vsync
diff --git a/doc/derived_measurements.rst b/doc/derived_measurements.rst
index 0a678bb..d56f94b 100644
--- a/doc/derived_measurements.rst
+++ b/doc/derived_measurements.rst
@@ -179,3 +179,43 @@
Please see the `gfxinfo documentation`_ for details.
.. _gfxinfo documentation: https://developer.android.com/training/testing/performance.html
+
+
+.. class:: DerivedSurfaceFlingerStats(drop_threshold=5, suffix='-fps', filename=None, outdir=None)
+
+ Produces FPS (frames-per-second) and other dervied statistics from
+ :class:`SurfaceFlingerFramesInstrument` 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:: DerivedSurfaceFlingerStats.process(measurement_csv)
+
+ Process the fames csv generated by :class:`SurfaceFlingerFramesInstrument` 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, followed by ``janks`` ``janks_pc``, and
+ ``missed_vsync`` metrics.