OLD | NEW |
---|---|
(Empty) | |
1 # Copyright 2015 The Chromium Authors. All rights reserved. | |
2 # Use of this source code is governed by a BSD-style license that can be | |
3 # found in the LICENSE file. | |
4 import numpy | |
5 import logging | |
6 | |
7 from telemetry.timeline import tracing_category_filter | |
8 from telemetry.timeline import tracing_options | |
9 from telemetry.timeline import model | |
10 from telemetry.page import page_test | |
11 from telemetry.value import scalar | |
12 from telemetry.value import list_of_scalar_values | |
13 from telemetry.value import list_of_string_values | |
14 from metrics import power | |
15 | |
16 | |
17 DISPLAY_HERTZ = 60.0 | |
18 # When to consider a frame frozen (in VSYNC units): | |
19 # meaning 1 initial frame + 5 repeats of that frame. | |
20 FROZEN_THRESHOLD = 6 | |
21 # Severity factor. | |
22 SEVERITY = 3 | |
23 | |
24 | |
25 class WebMediaPlayerMsRenderingStats(object): | |
26 """Analyzes events of WebMediaPlayerMs type.""" | |
27 | |
28 def __init__(self, events): | |
29 """Save relevant events.""" | |
30 self.relevant_events = events | |
31 | |
32 def InferCadence(self): | |
33 """Calculate the apparent cadence of the rendering.""" | |
34 # Term 'cadence' loosely used here for lack of a better word. | |
35 cadence = [] | |
36 frame_persistence = 0 | |
37 old_ideal_render = 0 | |
38 for event in self.relevant_events: | |
39 if (event.args and 'Ideal Render Instant' in event.args | |
40 and event.args['Ideal Render Instant'] == old_ideal_render): | |
41 frame_persistence += 1 | |
42 elif event.args and 'Ideal Render Instant' in event.args: | |
43 cadence.append(frame_persistence) | |
44 frame_persistence = 1 | |
45 old_ideal_render = event.args['Ideal Render Instant'] | |
46 cadence.append(frame_persistence) | |
47 cadence.pop(0) | |
48 return cadence | |
49 | |
50 def Bucketize(self, cadence): | |
51 """Create distribution for the cadence frame display values.""" | |
52 # If the overall display distribution is A1:A2:..:An, | |
53 # this will tell us how many times a frame | |
54 # stays displayed during Ak vsync duration (i.e. Ak/DISPLAY_HERTZ) | |
55 # also known as 'source to output' distribution. | |
56 bucket = {} | |
57 for ticks in cadence: | |
58 if ticks in bucket: | |
59 bucket[ticks] += 1 | |
60 else: | |
61 bucket[ticks] = 1 | |
62 return bucket | |
63 | |
64 def InferFpsFromCadence(self, bucket): | |
65 """Calculate the apparent FPS from cadence pattern.""" | |
66 # The mean ratio is the barycenter | |
67 weight = sum([bucket[ticks] for ticks in bucket]) | |
qiangchen
2015/08/28 21:26:01
Possibly name this variable as num_frames.
cpaulin (no longer in chrome)
2015/09/16 22:57:04
Done.
| |
68 population= sum([ticks * bucket[ticks] for ticks in bucket]) | |
qiangchen
2015/08/28 21:26:01
Possibly name this variable as num_vsyncs.
cpaulin (no longer in chrome)
2015/09/16 22:57:04
Done.
| |
69 mean_ratio = float(population) / weight | |
70 fps = DISPLAY_HERTZ / mean_ratio | |
71 return fps | |
72 | |
73 def InferFrozenFramesEvents(self, bucket): | |
74 """Find evidence of frozen frames in distribution.""" | |
75 # For simplicity we count as freezing the frames | |
76 # that appear at least five times in a row | |
77 # counted from 'Ideal Render Instant' perspective. | |
78 frozen_frames = [] | |
79 for ticks in bucket: | |
80 if ticks >= FROZEN_THRESHOLD: | |
81 logging.error('%s frames not updated after %s vsyncs', | |
82 bucket[ticks], ticks) | |
83 frozen_frames.append( | |
84 {'frozen_frames' : ticks -1 , | |
qiangchen
2015/08/28 21:26:01
Question: why we take ticks-1 here?
cpaulin (no longer in chrome)
2015/09/16 22:57:05
Because if you have 1 source frame and it displaye
| |
85 'occurences' : bucket[ticks]}) | |
86 return frozen_frames | |
87 | |
88 def FrozenPenaltyWeight(self, number_frozen_frames): | |
89 """Returns the weighted penalty for a number of frozen frames.""" | |
90 # As mentioned earlier, we count for frozen anything above 6 vsync | |
91 # display duration for the same 'Initial Render Instant'. | |
92 penalty = { | |
93 0 : 0, | |
94 1 : 0, | |
95 2 : 0, | |
96 3 : 0, | |
97 4 : 0, | |
98 5 : 1, | |
99 6 : 5, | |
100 7 : 15, | |
101 8 : 25 | |
102 } | |
103 weight = penalty.get(number_frozen_frames, | |
104 8 * (number_frozen_frames - 4)) | |
105 return weight | |
106 | |
107 def InferTimeStats(self): | |
108 """Calculate time stamp stats for all events.""" | |
109 | |
110 cadence = self.InferCadence() | |
111 bucket = self.Bucketize(cadence) | |
112 fps = self.InferFpsFromCadence(bucket) | |
113 frozen_frames = self.InferFrozenFramesEvents(bucket) | |
114 # Drift time between Ideal Render Instant and Actual Render Begin. | |
115 drift_time = [] | |
116 old_ideal_render = 0 | |
117 discrepancy = [] | |
118 index = 0 | |
119 for event in self.relevant_events: | |
120 current_ideal_render = event.args['Ideal Render Instant'] | |
121 if current_ideal_render == old_ideal_render: | |
122 continue | |
123 drift_time.append( | |
124 event.args['Actual Render Begin'] - current_ideal_render) | |
125 discrepancy.append(abs(current_ideal_render - old_ideal_render | |
126 - 1e6 / DISPLAY_HERTZ * cadence[index])) | |
127 old_ideal_render = current_ideal_render | |
128 index += 1 | |
129 discrepancy.pop(0) | |
130 last_ideal_render = self.relevant_events[-1].args[ | |
131 'Ideal Render Instant'] | |
132 first_ideal_render = self.relevant_events[0].args[ | |
133 'Ideal Render Instant'] | |
134 rendering_length_error = 100.0 * (sum([x for x in discrepancy]) / | |
135 (last_ideal_render - first_ideal_render)) | |
136 # Some stats on drift time. | |
137 mean_drift_time = numpy.mean(drift_time) | |
138 std_dev_drift_time = numpy.std(drift_time) | |
139 norm_drift_time = [abs(x - mean_drift_time) for x in drift_time] | |
140 # How many times is a frame later/earlier than T=2/DISPLAY_HERTZ. | |
141 # Time is in microseconds. | |
142 frames_severely_out_of_sync = len( | |
143 [x for x in norm_drift_time if abs(x) > 2e6 / DISPLAY_HERTZ]) | |
qiangchen
2015/08/28 21:26:01
How about defining a variable say vsync_duration =
| |
144 percent_badly_oos = ( | |
145 100.0 * frames_severely_out_of_sync / len(norm_drift_time)) | |
146 # How many times is a frame later/earlier than 1/DISPLAY_HERTZ. | |
147 frames_out_of_sync = len( | |
148 [x for x in norm_drift_time if abs(x) > 1e6 / (DISPLAY_HERTZ)]) | |
149 percent_out_of_sync = ( | |
150 100.0 * frames_out_of_sync / len(norm_drift_time)) | |
151 | |
152 frames_oos_only_once = frames_out_of_sync - frames_severely_out_of_sync | |
153 # For safety I don't use population = len(self.relevant_events) just | |
154 # in case other events are added later. | |
155 population = sum([n * bucket[n] for n in bucket]) | |
qiangchen
2015/08/28 21:26:01
ditto
| |
156 # Calculate smoothness metric. | |
157 # From the formula, we can see that smoothness score can be negative. | |
158 smoothness_score = 100.0 - 100.0*(frames_oos_only_once + | |
159 SEVERITY * frames_severely_out_of_sync) / len(norm_drift_time) | |
160 # Calculate freezing metric. | |
161 # Freezing metric can be negative if things are really bad. | |
162 freezing_score = 100.0 | |
163 for frozen_report in frozen_frames: | |
164 weight = self.FrozenPenaltyWeight(frozen_report['frozen_frames']) | |
165 freezing_score -= ( | |
166 100.0 * frozen_report['occurences'] / population * weight) | |
167 | |
168 stats = { | |
169 'drift_time': drift_time, | |
170 'mean_drift_time' : mean_drift_time, | |
171 'std_dev_drift_time' : std_dev_drift_time, | |
172 'percent_badly_out_of_sync' : percent_badly_oos, | |
173 'percent_out_of_sync' : percent_out_of_sync, | |
174 'smoothness_score' : smoothness_score, | |
175 'freezing_score' : freezing_score, | |
176 'rendering_length_error' : rendering_length_error, | |
177 'fps' : fps, | |
178 'bucket' : bucket} | |
179 return stats | |
180 | |
181 | |
182 class WebRTCRendering(page_test.PageTest): | |
nednguyen
2015/09/02 16:26:46
Please don't add another page_test & use TimelineB
phoglund_chromium
2015/09/04 09:16:24
Right, and so most of this code should be a Metric
| |
183 """Gathers WebRTC video rendering-related metrics on a page set.""" | |
184 | |
185 def __init__(self): | |
186 super(WebRTCRendering, self).__init__() | |
187 self._power_metric = None | |
188 self._webrtc_rendering_metric = None | |
189 | |
190 def WillStartBrowser(self, platform): | |
191 self._power_metric = power.PowerMetric(platform) | |
192 | |
193 def WillNavigateToPage(self, page, tab): | |
194 self._power_metric.Start(page, tab) | |
195 | |
196 options = tracing_options.TracingOptions() | |
197 options.enable_chrome_trace = True | |
198 # FIXME: Remove the timeline category when impl-side painting is on | |
199 # everywhere. | |
200 category_filter = tracing_category_filter.TracingCategoryFilter('cc') | |
201 tab.browser.platform.tracing_controller.Start(options, category_filter) | |
202 | |
203 def CustomizeBrowserOptions(self, options): | |
204 #options.AppendExtraBrowserArgs('--use-fake-device-for-media-stream') | |
205 options.AppendExtraBrowserArgs('--use-fake-ui-for-media-stream') | |
206 power.PowerMetric.CustomizeBrowserOptions(options) | |
207 | |
208 def StopBrowserAfterPage(self, browser, page): | |
209 # Restart the browser after the last page in the pageset. | |
210 return True | |
211 | |
212 def ValidateAndMeasurePage(self, page, tab, results): | |
213 timeline_data = tab.browser.platform.tracing_controller.Stop() | |
214 timeline_model = model.TimelineModel(timeline_data) | |
215 self._power_metric.Stop(page, tab) | |
216 self._power_metric.AddResults(tab, results) | |
217 rendering_events = timeline_model.GetAllEventsOfName( | |
218 'WebMediaPlayerMS::UpdateCurrentFrame') | |
219 if not rendering_events: | |
220 rendering_events = [{'args':{'Unkown':'Unknown'}}] | |
221 # TBD what to do next when no events exist? | |
222 event_parser = WebMediaPlayerMsRenderingStats(rendering_events) | |
223 rendering_stats = event_parser.InferTimeStats() | |
224 logging.info ("rendering stats : %s", rendering_stats) | |
225 results.AddValue(list_of_scalar_values.ListOfScalarValues( | |
226 results.current_page, | |
227 'WebRtcRendering_drift_time', | |
228 'usec', | |
229 rendering_stats['drift_time'], | |
230 important=True, | |
231 description='Drift time for a rendered frame')) | |
232 | |
233 results.AddValue(scalar.ScalarValue( | |
234 results.current_page, | |
235 'WebRTCRendering_mean_drift_time', | |
236 'usec', | |
237 rendering_stats['mean_drift_time'], | |
238 important=True, | |
239 description='Mean drift time for frames')) | |
240 | |
241 results.AddValue(scalar.ScalarValue( | |
242 results.current_page, | |
243 'WebRTCRendering_std_dev_drift_time', | |
244 'usec', | |
245 rendering_stats['std_dev_drift_time'], | |
246 important=True, | |
247 description='Standard deviation of drift time for frames')) | |
248 | |
249 results.AddValue(scalar.ScalarValue( | |
250 results.current_page, | |
251 'WebRTCRendering_percent_badly_out_of_sync', | |
252 '%', | |
253 rendering_stats['percent_badly_out_of_sync'], | |
254 important=True, | |
255 description='Percentage of frame which drifted more than 2 VSYNC')) | |
256 | |
257 results.AddValue(scalar.ScalarValue( | |
258 results.current_page, | |
259 'WebRTCRendering_percent_out_of_sync', | |
260 '%', | |
261 rendering_stats['percent_out_of_sync'], | |
262 important=True, | |
263 description='Percentage of frame which drifted more than 1 VSYNC')) | |
264 | |
265 # make the output distribution a list since | |
266 # no facilities for dict values exist (yet) | |
267 bucket_list = [] | |
268 for key, value in rendering_stats['bucket'].iteritems(): | |
269 temp = "%s:%s" % (key,value) | |
270 bucket_list.append(temp) | |
271 results.AddValue(list_of_string_values.ListOfStringValues( | |
272 results.current_page, | |
273 'WebRtcRendering_bucket', | |
274 'frames:occurences', | |
275 bucket_list, | |
276 important=True, | |
277 description='Output distribution of frames')) | |
278 | |
279 results.AddValue(scalar.ScalarValue( | |
280 results.current_page, | |
281 'WebRTCRendering_fps', | |
282 'FPS', | |
283 rendering_stats['fps'], | |
284 important=True, | |
285 description='Calculated Frame Rate of video rendering')) | |
286 | |
287 results.AddValue(scalar.ScalarValue( | |
288 results.current_page, | |
289 'WebRTCRendering_smoothness_score', | |
290 '%', | |
291 rendering_stats['smoothness_score'], | |
292 important=True, | |
293 description='Smoothness score of rendering')) | |
294 | |
295 results.AddValue(scalar.ScalarValue( | |
296 results.current_page, | |
297 'WebRTCRendering_freezing_score', | |
298 '%', | |
299 rendering_stats['freezing_score'], | |
300 important=True, | |
301 description='Freezing score of rendering')) | |
302 | |
303 results.AddValue(scalar.ScalarValue( | |
304 results.current_page, | |
305 'WebRTCRendering_rendering_length_error', | |
306 '%', | |
307 rendering_stats['rendering_length_error'], | |
308 important=True, | |
309 description='Rendering length error rate')) | |
310 | |
311 def CleanUpAfterPage(self, page, tab): | |
312 tracing_controller = tab.browser.platform.tracing_controller | |
313 if tracing_controller.is_tracing_running: | |
314 tracing_controller.Stop() | |
OLD | NEW |