Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(25)

Side by Side Diff: tools/telemetry/telemetry/web_perf/metrics/webrtc_rendering_stats.py

Issue 1254023003: Telemetry Test for WebRTC Rendering. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Added unit test to CL Created 5 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(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
5 import logging
6
7 import numpy
8
9 DISPLAY_HERTZ = 60.0
10 VSYNC_DURATION = 1e6 / DISPLAY_HERTZ
11 # When to consider a frame frozen (in VSYNC units): meaning 1 initial
12 # frame + 5 repeats of that frame.
13 FROZEN_THRESHOLD = 6
14 # Severity factor.
15 SEVERITY = 3
16
17
18 class WebMediaPlayerMsRenderingStats(object):
19 """Analyzes events of WebMediaPlayerMs type."""
20
21 def __init__(self, events):
22 """Save relevant events according to their stream."""
23 self.stream_to_events = self._MapEventsToStream(events)
24
25 def _IsEventValid(self, event):
26 """Check that the needed arguments are present in event.
27
28 Args:
29 event: event to check.
30
31 Returns:
32 True is event is valid, false otherwise."""
33 if not event.args:
34 return False
35 mandatory = ['Actual Render Begin', 'Actual Render End',
36 'Ideal Render Instant', 'Serial']
37 for parameter in mandatory:
38 if not parameter in event.args:
39 return False
40 return True
41
42 def _MapEventsToStream(self, events):
43 """Build a dictionary of events indexed by stream.
44
45 The events of interest have a 'Serial' argument which represents the
46 stream ID. The 'Serial' argument identifies the local or remote nature of
47 the stream with a single digit 0 or 1 as well as the URL blob associated
48 with the video stream. So stream::=(0|1}blob . The method will then list
49 the events of the same stream in a frame_distribution on stream id.
50 Args:
51 events: Telemetry WebMediaPlayerMs events.
52
53 Returns:
54 A dict of stream IDs mapped to events on that stream.
55 """
56 stream_to_events = {}
57 for event in events:
58 if not self._IsEventValid(event):
59
phoglund_chromium 2015/09/24 07:11:46 Nit: this would be an exception to the rule of "bl
cpaulin (no longer in chrome) 2015/09/24 21:47:38 Done.
60 # This is not a render event, skip it.
61 continue
62 stream = event.args['Serial']
63 events_for_stream = stream_to_events.setdefault(stream, [])
64 events_for_stream.append(event)
65
66 return stream_to_events
67
68 def _GetCadence(self, relevant_events):
69 """Calculate the apparent cadence of the rendering.
70
71 In this paragraph I will be using regex notation. What is intended by the
72 word cadence is a sort of extended instantaneous 'Cadence' (thus not
73 necessarily periodic). Just as an example, a normal 'Cadence' could be
74 something like [2 3] which means possibly an observed frame persistance
75 progression of [{2 3}+] for an ideal 20FPS video source. So what we are
76 calculating here is the list of frame persistance, kind of a
77 'Proto-Cadence', but cadence is shorter so we abuse the word.
78
79 Args:
80 relevant_events: list of Telemetry events.
81
82 Returns:
83 a list of frame persistance values.
84 """
85 cadence = []
86 frame_persistence = 0
87 old_ideal_render = 0
88 ideal_render_instant = 'Ideal Render Instant'
89 for event in relevant_events:
90 if not self._IsEventValid(event):
91
92 # This event is not a render event so skip it.
93 continue
94 if event.args[ideal_render_instant] == old_ideal_render:
95 frame_persistence += 1
96 else:
97 cadence.append(frame_persistence)
98 frame_persistence = 1
99 old_ideal_render = event.args[ideal_render_instant]
100 cadence.append(frame_persistence)
101 cadence.pop(0)
102 return cadence
103
104 def _GetSourceToOutputDistribution(self, cadence):
105 """Create distribution for the cadence frame display values.
106
107 If the overall display distribution is A1:A2:..:An, this will tell us how
108 many times a frame stays displayed during Ak*VSYNC_DURATION, also known as
109 'source to output' distribution. Or in other terms:
110 a distribution B::= let C be the cadence, B[k]=p with k in Unique(C)
111 and p=Card(k in C).
112
113 Args:
114 cadence: list of frame persistance values.
115
116 Returns:
117 a dictionary containing the distribution
118
phoglund_chromium 2015/09/24 07:11:46 Nit: remove blank line
cpaulin (no longer in chrome) 2015/09/24 21:47:38 Done.
119 """
phoglund_chromium 2015/09/24 07:11:46 Nit: remove blank line
cpaulin (no longer in chrome) 2015/09/24 21:47:38 Done.
120
121 frame_distribution = {}
122 for ticks in cadence:
123 ticks_so_far = frame_distribution.setdefault(ticks, 0)
124 frame_distribution[ticks] = ticks_so_far + 1
125 return frame_distribution
126
127 def _GetFpsFromCadence(self, frame_distribution):
128 """Calculate the apparent FPS from frame distribution.
129
130 Knowing the display frequency and the frame distribution, it is possible to
131 calculate the video apparent frame rate as played by WebMediaPlayerMs
132 module.
133
134 Args:
135 frame_distribution: the source to output distribution.
136
137 Returns:
138 the video apparent frame rate.
139 """
140 number_frames = sum(frame_distribution.values())
141 number_vsyncs = sum([ticks * frame_distribution[ticks]
142 for ticks in frame_distribution])
143 mean_ratio = float(number_vsyncs) / number_frames
144 return DISPLAY_HERTZ / mean_ratio
145
146 def _GetFrozenFramesReports(self, frame_distribution):
147 """Find evidence of frozen frames in distribution.
148
149 For simplicity we count as freezing the frames that appear at least five
150 times in a row counted from 'Ideal Render Instant' perspective. So let's
151 say for 1 source frame, we rendered 6 frames, then we consider 5 of these
152 rendered frames as frozen. But we mitigate this by saying anything under
153 5 frozen frames will not be counted as frozen.
154
155 Args:
156 frame_distribution: the source to output distribution.
157
158 Returns:
159 a list of dicts whose keys are ('frozen_frames', 'occurrences').
160 """
161
162 frozen_frames = []
163 frozen_frame_vsyncs = [ticks for ticks in frame_distribution if ticks >=
164 FROZEN_THRESHOLD]
165 for frozen_frames_vsync in frozen_frame_vsyncs:
166 logging.debug('%s frames not updated after %s vsyncs',
167 frame_distribution[frozen_frames_vsync], frozen_frames_vsync)
168 frozen_frames.append(
169 {'frozen_frames': frozen_frames_vsync - 1,
170 'occurrences': frame_distribution[frozen_frames_vsync]})
171 return frozen_frames
172
173 def _FrozenPenaltyWeight(self, number_frozen_frames):
174 """Returns the weighted penalty for a number of frozen frames.
175
176 As mentioned earlier, we count for frozen anything above 6 vsync display
177 duration for the same 'Initial Render Instant', which is five frozen
178 frames.
179
180 Args:
181 number_frozen_frames: number of frozen frames.
182
183 Returns:
184 the penalty weight (int) for that number of frozen frames.
185 """
186
187 penalty = {
188 0: 0,
189 1: 0,
190 2: 0,
191 3: 0,
192 4: 0,
193 5: 1,
194 6: 5,
195 7: 15,
196 8: 25
197 }
198 weight = penalty.get(number_frozen_frames, 8 * (number_frozen_frames - 4))
199 return weight
200
201 def _IsRemoteStream(self, stream):
202 """Check if stream is remote."""
203 return stream[0] == '1'
204
205 def _GetDrifTimeStats(self, relevant_events, cadence):
206 """Get the drift time statistics.
207
208 This method will calculate drift_time stats, that is to say :
209 drift_time::= list(actual render begin - ideal render).
210 rendering_length error::= the rendering length error.
211
212 Args:
213 relevant_events: events to get drift times stats from.
214 cadence: list of frame persistence values.
215
216 Returns:
217 a tuple of (drift_time, rendering_length_error).
218 """
219 drift_time = []
220 old_ideal_render = 0
221 discrepancy = []
222 index = 0
223 ideal_render_instant = 'Ideal Render Instant'
224 for event in relevant_events:
225 current_ideal_render = event.args[ideal_render_instant]
226 if current_ideal_render == old_ideal_render:
227
228 # Skip to next event because we're looking for a source frame.
229 continue
230 actual_render_begin = event.args['Actual Render Begin']
231 drift_time.append(actual_render_begin - current_ideal_render)
232 discrepancy.append(abs(current_ideal_render - old_ideal_render
233 - VSYNC_DURATION * cadence[index]))
234 old_ideal_render = current_ideal_render
235 index += 1
236 discrepancy.pop(0)
237 last_ideal_render = relevant_events[-1].args[ideal_render_instant]
238 first_ideal_render = relevant_events[0].args[ideal_render_instant]
239 rendering_length_error = 100.0 * (sum([x for x in discrepancy]) /
240 (last_ideal_render - first_ideal_render))
241
242 return (drift_time, rendering_length_error)
phoglund_chromium 2015/09/24 07:11:46 Nit: It's fine to drop the () here.
cpaulin (no longer in chrome) 2015/09/24 21:47:38 Done.
243
244 def _GetSmoothnessStats(self, norm_drift_time):
245 """Get the smoothness stats from the normalized drift time.
246
247 This method will calculate the smoothness score, along with the percentage
248 of frames badly out of sync and the percentage of frames out of sync. To be
249 considered badly out of sync, a frame has to have missed rendering by at
250 least 2*VSYNC_DURATION. To be considered out of sync, a frame has to have
251 missed rendering by at least one VSYNC_DURATION.
252 The smoothness score is a measure of how out of sync the frames are.
253
254 Args:
255 norm_drift_time: normalized drift time.
256
257 Returns:
258 a tuple of (percent_badly_oos, percent_out_of_sync, smoothness_score)
259 """
260
261 # How many times is a frame later/earlier than T=2*VSYNC_DURATION. Time is
262 # in microseconds.
263 frames_severely_out_of_sync = len(
264 [x for x in norm_drift_time if abs(x) > 2 * VSYNC_DURATION])
265 percent_badly_oos = (
266 100.0 * frames_severely_out_of_sync / len(norm_drift_time))
267
268 # How many times is a frame later/earlier than VSYNC_DURATION.
269 frames_out_of_sync = len(
270 [x for x in norm_drift_time if abs(x) > VSYNC_DURATION])
271 percent_out_of_sync = (
272 100.0 * frames_out_of_sync / len(norm_drift_time))
273
274 frames_oos_only_once = frames_out_of_sync - frames_severely_out_of_sync
275
276 # Calculate smoothness metric. From the formula, we can see that smoothness
277 # score can be negative.
278 smoothness_score = 100.0 - 100.0 * (frames_oos_only_once +
279 SEVERITY * frames_severely_out_of_sync) / len(norm_drift_time)
280
281 # Minimum smoothness_score value allowed is zero.
282 if smoothness_score < 0:
283 smoothness_score = 0
284
285 return (percent_badly_oos, percent_out_of_sync, smoothness_score)
286
287 def _GetFreezingScore(self, frame_distribution):
288 """Get the freezing score."""
289
290 # The freezing score is based on the source to output distribution.
291 number_vsyncs = sum([n * frame_distribution[n]
292 for n in frame_distribution])
293 frozen_frames = self._GetFrozenFramesReports(frame_distribution)
294
295 # Calculate freezing metric.
296 # Freezing metric can be negative if things are really bad. In that case we
297 # change it to zero as minimum valud.
298 freezing_score = 100.0
299 for frozen_report in frozen_frames:
300 weight = self._FrozenPenaltyWeight(frozen_report['frozen_frames'])
301 freezing_score -= (
302 100.0 * frozen_report['occurrences'] / number_vsyncs * weight)
303 if freezing_score < 0:
304 freezing_score = 0
305
306 return freezing_score
307
308 def GetTimeStats(self):
309 """Calculate time stamp stats for all remote stream events."""
310 stats = {}
311 for stream, relevant_events in self.stream_to_events.iteritems():
312 if len(relevant_events) == 1:
313 logging.debug('Found a stream=%s with just one event', stream)
314 continue
315 if not self._IsRemoteStream(stream):
316 logging.info('Skipping processing of local stream: %s', stream)
317 continue
318
319 cadence = self._GetCadence(relevant_events)
320 frame_distribution = self._GetSourceToOutputDistribution(cadence)
321 fps = self._GetFpsFromCadence(frame_distribution)
322
323 drift_time_stats = self._GetDrifTimeStats(relevant_events, cadence)
324 (drift_time, rendering_length_error) = drift_time_stats
325
326 # Drift time normalization.
327 mean_drift_time = numpy.mean(drift_time)
328 norm_drift_time = [abs(x - mean_drift_time) for x in drift_time]
329
330 smoothness_stats = self._GetSmoothnessStats(norm_drift_time)
331 (percent_badly_oos, percent_out_of_sync,
332 smoothness_score) = smoothness_stats
333
334 freezing_score = self._GetFreezingScore(frame_distribution)
335
336 stats = {
337 'drift_time': drift_time,
338 'mean_drift_time': mean_drift_time,
339 'std_dev_drift_time': numpy.std(drift_time),
340 'percent_badly_out_of_sync': percent_badly_oos,
341 'percent_out_of_sync': percent_out_of_sync,
342 'smoothness_score': smoothness_score,
343 'freezing_score': freezing_score,
344 'rendering_length_error': rendering_length_error,
345 'fps': fps,
346 'frame_distribution': frame_distribution}
347 print "Stats for remote stream {0}: {1}".format(stream, stats)
348 return stats
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698