Sergei Trofimov | f692315 | 2017-09-07 17:36:18 +0100 | [diff] [blame] | 1 | from __future__ import division |
| 2 | import csv |
| 3 | import os |
| 4 | import re |
| 5 | |
| 6 | try: |
| 7 | import pandas as pd |
| 8 | except ImportError: |
| 9 | pd = None |
| 10 | |
| 11 | from devlib import DerivedMeasurements, DerivedMetric, MeasurementsCsv, InstrumentChannel |
Sergei Trofimov | 9f66632 | 2017-09-13 11:45:55 +0100 | [diff] [blame] | 12 | from devlib.exception import HostError |
Sergei Trofimov | f692315 | 2017-09-07 17:36:18 +0100 | [diff] [blame] | 13 | from devlib.utils.rendering import gfxinfo_get_last_dump, VSYNC_INTERVAL |
| 14 | from devlib.utils.types import numeric |
| 15 | |
| 16 | |
| 17 | class DerivedFpsStats(DerivedMeasurements): |
| 18 | |
| 19 | def __init__(self, drop_threshold=5, suffix=None, filename=None, outdir=None): |
| 20 | self.drop_threshold = drop_threshold |
| 21 | self.suffix = suffix |
| 22 | self.filename = filename |
| 23 | self.outdir = outdir |
| 24 | if (filename is None) and (suffix is None): |
| 25 | self.suffix = '-fps' |
| 26 | elif (filename is not None) and (suffix is not None): |
| 27 | raise ValueError('suffix and filename cannot be specified at the same time.') |
| 28 | if filename is not None and os.sep in filename: |
| 29 | raise ValueError('filename cannot be a path (cannot countain "{}"'.format(os.sep)) |
| 30 | |
| 31 | def process(self, measurements_csv): |
| 32 | if isinstance(measurements_csv, basestring): |
| 33 | measurements_csv = MeasurementsCsv(measurements_csv) |
| 34 | if pd is not None: |
| 35 | return self._process_with_pandas(measurements_csv) |
| 36 | return self._process_without_pandas(measurements_csv) |
| 37 | |
| 38 | def _get_csv_file_name(self, frames_file): |
| 39 | outdir = self.outdir or os.path.dirname(frames_file) |
| 40 | if self.filename: |
| 41 | return os.path.join(outdir, self.filename) |
| 42 | |
| 43 | frames_basename = os.path.basename(frames_file) |
| 44 | rest, ext = os.path.splitext(frames_basename) |
| 45 | csv_basename = rest + self.suffix + ext |
| 46 | return os.path.join(outdir, csv_basename) |
| 47 | |
| 48 | |
| 49 | class DerivedGfxInfoStats(DerivedFpsStats): |
| 50 | |
| 51 | @staticmethod |
| 52 | def process_raw(filepath, *args): |
| 53 | metrics = [] |
| 54 | dump = gfxinfo_get_last_dump(filepath) |
| 55 | seen_stats = False |
| 56 | for line in dump.split('\n'): |
| 57 | if seen_stats and not line.strip(): |
| 58 | break |
| 59 | elif line.startswith('Janky frames:'): |
| 60 | text = line.split(': ')[-1] |
| 61 | val_text, pc_text = text.split('(') |
| 62 | metrics.append(DerivedMetric('janks', numeric(val_text.strip()), 'count')) |
| 63 | metrics.append(DerivedMetric('janks_pc', numeric(pc_text[:-3]), 'percent')) |
| 64 | elif ' percentile: ' in line: |
| 65 | ptile, val_text = line.split(' percentile: ') |
| 66 | name = 'render_time_{}_ptile'.format(ptile) |
| 67 | value = numeric(val_text.strip()[:-2]) |
| 68 | metrics.append(DerivedMetric(name, value, 'time_ms')) |
| 69 | elif line.startswith('Number '): |
| 70 | name_text, val_text = line.strip().split(': ') |
| 71 | name = name_text[7:].lower().replace(' ', '_') |
| 72 | value = numeric(val_text) |
| 73 | metrics.append(DerivedMetric(name, value, 'count')) |
| 74 | else: |
| 75 | continue |
| 76 | seen_stats = True |
| 77 | return metrics |
| 78 | |
| 79 | def _process_without_pandas(self, measurements_csv): |
| 80 | per_frame_fps = [] |
| 81 | start_vsync, end_vsync = None, None |
| 82 | frame_count = 0 |
| 83 | |
| 84 | for frame_data in measurements_csv.iter_values(): |
| 85 | if frame_data.Flags_flags != 0: |
| 86 | continue |
| 87 | frame_count += 1 |
| 88 | |
| 89 | if start_vsync is None: |
| 90 | start_vsync = frame_data.Vsync_time_us |
| 91 | end_vsync = frame_data.Vsync_time_us |
| 92 | |
| 93 | frame_time = frame_data.FrameCompleted_time_us - frame_data.IntendedVsync_time_us |
| 94 | pff = 1e9 / frame_time |
| 95 | if pff > self.drop_threshold: |
| 96 | per_frame_fps.append([pff]) |
| 97 | |
| 98 | if frame_count: |
| 99 | duration = end_vsync - start_vsync |
Sergei Trofimov | 4593d86 | 2017-09-22 13:46:32 +0100 | [diff] [blame] | 100 | fps = (1e6 * frame_count) / float(duration) |
Sergei Trofimov | f692315 | 2017-09-07 17:36:18 +0100 | [diff] [blame] | 101 | else: |
| 102 | duration = 0 |
| 103 | fps = 0 |
| 104 | |
| 105 | csv_file = self._get_csv_file_name(measurements_csv.path) |
| 106 | with open(csv_file, 'wb') as wfh: |
| 107 | writer = csv.writer(wfh) |
| 108 | writer.writerow(['fps']) |
| 109 | writer.writerows(per_frame_fps) |
| 110 | |
| 111 | return [DerivedMetric('fps', fps, 'fps'), |
| 112 | DerivedMetric('total_frames', frame_count, 'frames'), |
| 113 | MeasurementsCsv(csv_file)] |
| 114 | |
| 115 | def _process_with_pandas(self, measurements_csv): |
| 116 | data = pd.read_csv(measurements_csv.path) |
| 117 | data = data[data.Flags_flags == 0] |
| 118 | frame_time = data.FrameCompleted_time_us - data.IntendedVsync_time_us |
Sergei Trofimov | 4593d86 | 2017-09-22 13:46:32 +0100 | [diff] [blame] | 119 | per_frame_fps = (1e6 / frame_time) |
Sergei Trofimov | f692315 | 2017-09-07 17:36:18 +0100 | [diff] [blame] | 120 | keep_filter = per_frame_fps > self.drop_threshold |
| 121 | per_frame_fps = per_frame_fps[keep_filter] |
| 122 | per_frame_fps.name = 'fps' |
| 123 | |
| 124 | frame_count = data.index.size |
| 125 | if frame_count > 1: |
| 126 | duration = data.Vsync_time_us.iloc[-1] - data.Vsync_time_us.iloc[0] |
| 127 | fps = (1e9 * frame_count) / float(duration) |
| 128 | else: |
| 129 | duration = 0 |
| 130 | fps = 0 |
| 131 | |
| 132 | csv_file = self._get_csv_file_name(measurements_csv.path) |
| 133 | per_frame_fps.to_csv(csv_file, index=False, header=True) |
| 134 | |
| 135 | return [DerivedMetric('fps', fps, 'fps'), |
| 136 | DerivedMetric('total_frames', frame_count, 'frames'), |
| 137 | MeasurementsCsv(csv_file)] |
| 138 | |
Sergei Trofimov | 9f66632 | 2017-09-13 11:45:55 +0100 | [diff] [blame] | 139 | |
| 140 | class DerivedSurfaceFlingerStats(DerivedFpsStats): |
| 141 | |
| 142 | def _process_with_pandas(self, measurements_csv): |
| 143 | data = pd.read_csv(measurements_csv.path) |
| 144 | |
| 145 | # fiter out bogus frames. |
| 146 | bogus_frames_filter = data.actual_present_time_us != 0x7fffffffffffffff |
| 147 | actual_present_times = data.actual_present_time_us[bogus_frames_filter] |
| 148 | actual_present_time_deltas = actual_present_times.diff().dropna() |
| 149 | |
| 150 | vsyncs_to_compose = actual_present_time_deltas.div(VSYNC_INTERVAL) |
| 151 | vsyncs_to_compose.apply(lambda x: int(round(x, 0))) |
| 152 | |
| 153 | # drop values lower than drop_threshold FPS as real in-game frame |
| 154 | # rate is unlikely to drop below that (except on loading screens |
| 155 | # etc, which should not be factored in frame rate calculation). |
| 156 | per_frame_fps = (1.0 / (vsyncs_to_compose.multiply(VSYNC_INTERVAL / 1e9))) |
| 157 | keep_filter = per_frame_fps > self.drop_threshold |
| 158 | filtered_vsyncs_to_compose = vsyncs_to_compose[keep_filter] |
| 159 | per_frame_fps.name = 'fps' |
| 160 | |
| 161 | csv_file = self._get_csv_file_name(measurements_csv.path) |
| 162 | per_frame_fps.to_csv(csv_file, index=False, header=True) |
| 163 | |
| 164 | if not filtered_vsyncs_to_compose.empty: |
| 165 | fps = 0 |
| 166 | total_vsyncs = filtered_vsyncs_to_compose.sum() |
| 167 | frame_count = filtered_vsyncs_to_compose.size |
| 168 | |
| 169 | if total_vsyncs: |
| 170 | fps = 1e9 * frame_count / (VSYNC_INTERVAL * total_vsyncs) |
| 171 | |
| 172 | janks = self._calc_janks(filtered_vsyncs_to_compose) |
| 173 | not_at_vsync = self._calc_not_at_vsync(vsyncs_to_compose) |
| 174 | else: |
| 175 | fps = 0 |
| 176 | frame_count = 0 |
| 177 | janks = 0 |
| 178 | not_at_vsync = 0 |
| 179 | |
| 180 | return [DerivedMetric('fps', fps, 'fps'), |
| 181 | DerivedMetric('total_frames', frame_count, 'frames'), |
| 182 | MeasurementsCsv(csv_file), |
| 183 | DerivedMetric('janks', janks, 'count'), |
| 184 | DerivedMetric('janks_pc', janks * 100 / frame_count, 'percent'), |
| 185 | DerivedMetric('missed_vsync', not_at_vsync, 'count')] |
| 186 | |
| 187 | def _process_without_pandas(self, measurements_csv): |
| 188 | # Given that SurfaceFlinger has been deprecated in favor of GfxInfo, |
| 189 | # it does not seem worth it implementing this. |
| 190 | raise HostError('Please install "pandas" Python package to process SurfaceFlinger frames') |
| 191 | |
| 192 | @staticmethod |
| 193 | def _calc_janks(filtered_vsyncs_to_compose): |
| 194 | """ |
| 195 | Internal method for calculating jank frames. |
| 196 | """ |
| 197 | pause_latency = 20 |
| 198 | vtc_deltas = filtered_vsyncs_to_compose.diff().dropna() |
| 199 | vtc_deltas = vtc_deltas.abs() |
| 200 | janks = vtc_deltas.apply(lambda x: (pause_latency > x > 1.5) and 1 or 0).sum() |
| 201 | |
| 202 | return janks |
| 203 | |
| 204 | @staticmethod |
| 205 | def _calc_not_at_vsync(vsyncs_to_compose): |
| 206 | """ |
| 207 | Internal method for calculating the number of frames that did not |
| 208 | render in a single vsync cycle. |
| 209 | """ |
| 210 | epsilon = 0.0001 |
| 211 | func = lambda x: (abs(x - 1.0) > epsilon) and 1 or 0 |
| 212 | not_at_vsync = vsyncs_to_compose.apply(func).sum() |
| 213 | |
| 214 | return not_at_vsync |