blob: e595926715326eb6e41e68457867bcd5688d870c [file] [log] [blame]
Sergei Trofimovf6923152017-09-07 17:36:18 +01001from __future__ import division
2import csv
3import os
4import re
5
6try:
7 import pandas as pd
8except ImportError:
9 pd = None
10
11from devlib import DerivedMeasurements, DerivedMetric, MeasurementsCsv, InstrumentChannel
Sergei Trofimov9f666322017-09-13 11:45:55 +010012from devlib.exception import HostError
Sergei Trofimovf6923152017-09-07 17:36:18 +010013from devlib.utils.rendering import gfxinfo_get_last_dump, VSYNC_INTERVAL
14from devlib.utils.types import numeric
15
16
17class 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
49class 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
100 fps = (1e9 * frame_count) / float(duration)
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
119 per_frame_fps = (1e9 / frame_time)
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 Trofimov9f666322017-09-13 11:45:55 +0100139
140class 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