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

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: Accomodate 'Serial' field definition change and minor fixes. Created 5 years, 3 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 least significant bit of 0 or 1 as well as the hash
48 value of the video track's URL. So stream::=hash(0|1} . The method will
49 then list the events of the same stream in a frame_distribution on stream
50 id. Practically speaking remote streams have an odd stream id and local
51 streams have a even stream id.
52 Args:
53 events: Telemetry WebMediaPlayerMs events.
54
55 Returns:
56 A dict of stream IDs mapped to events on that stream.
57 """
58 stream_to_events = {}
59 for event in events:
60 if not self._IsEventValid(event):
61 # This is not a render event, skip it.
62 continue
63 stream = event.args['Serial']
64 events_for_stream = stream_to_events.setdefault(stream, [])
65 events_for_stream.append(event)
66
67 return stream_to_events
68
69 def _GetCadence(self, relevant_events):
70 """Calculate the apparent cadence of the rendering.
71
72 In this paragraph I will be using regex notation. What is intended by the
73 word cadence is a sort of extended instantaneous 'Cadence' (thus not
74 necessarily periodic). Just as an example, a normal 'Cadence' could be
75 something like [2 3] which means possibly an observed frame persistance
76 progression of [{2 3}+] for an ideal 20FPS video source. So what we are
77 calculating here is the list of frame persistance, kind of a
78 'Proto-Cadence', but cadence is shorter so we abuse the word.
79
80 Args:
81 relevant_events: list of Telemetry events.
82
83 Returns:
84 a list of frame persistance values.
85 """
86 cadence = []
87 frame_persistence = 0
88 old_ideal_render = 0
89 ideal_render_instant = 'Ideal Render Instant'
nednguyen 2015/09/28 20:56:43 These constant strings should be declared at the t
cpaulin (no longer in chrome) 2015/09/29 15:50:25 Done.
90 for event in relevant_events:
91 if not self._IsEventValid(event):
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 """
119 frame_distribution = {}
120 for ticks in cadence:
121 ticks_so_far = frame_distribution.setdefault(ticks, 0)
122 frame_distribution[ticks] = ticks_so_far + 1
123 return frame_distribution
124
125 def _GetFpsFromCadence(self, frame_distribution):
126 """Calculate the apparent FPS from frame distribution.
127
128 Knowing the display frequency and the frame distribution, it is possible to
129 calculate the video apparent frame rate as played by WebMediaPlayerMs
130 module.
131
132 Args:
133 frame_distribution: the source to output distribution.
134
135 Returns:
136 the video apparent frame rate.
137 """
138 number_frames = sum(frame_distribution.values())
139 number_vsyncs = sum([ticks * frame_distribution[ticks]
140 for ticks in frame_distribution])
141 mean_ratio = float(number_vsyncs) / number_frames
142 return DISPLAY_HERTZ / mean_ratio
143
144 def _GetFrozenFramesReports(self, frame_distribution):
145 """Find evidence of frozen frames in distribution.
146
147 For simplicity we count as freezing the frames that appear at least five
148 times in a row counted from 'Ideal Render Instant' perspective. So let's
149 say for 1 source frame, we rendered 6 frames, then we consider 5 of these
150 rendered frames as frozen. But we mitigate this by saying anything under
151 5 frozen frames will not be counted as frozen.
152
153 Args:
154 frame_distribution: the source to output distribution.
155
156 Returns:
157 a list of dicts whose keys are ('frozen_frames', 'occurrences').
158 """
159 frozen_frames = []
160 frozen_frame_vsyncs = [ticks for ticks in frame_distribution if ticks >=
161 FROZEN_THRESHOLD]
162 for frozen_frames_vsync in frozen_frame_vsyncs:
163 logging.debug('%s frames not updated after %s vsyncs',
164 frame_distribution[frozen_frames_vsync], frozen_frames_vsync)
165 frozen_frames.append(
166 {'frozen_frames': frozen_frames_vsync - 1,
167 'occurrences': frame_distribution[frozen_frames_vsync]})
168 return frozen_frames
169
170 def _FrozenPenaltyWeight(self, number_frozen_frames):
171 """Returns the weighted penalty for a number of frozen frames.
172
173 As mentioned earlier, we count for frozen anything above 6 vsync display
174 duration for the same 'Initial Render Instant', which is five frozen
175 frames.
176
177 Args:
178 number_frozen_frames: number of frozen frames.
179
180 Returns:
181 the penalty weight (int) for that number of frozen frames.
182 """
183
184 penalty = {
185 0: 0,
186 1: 0,
187 2: 0,
188 3: 0,
189 4: 0,
190 5: 1,
191 6: 5,
192 7: 15,
193 8: 25
194 }
195 weight = penalty.get(number_frozen_frames, 8 * (number_frozen_frames - 4))
196 return weight
197
198 def _IsRemoteStream(self, stream):
199 """Check if stream is remote."""
200 return stream % 2
201
202 def _GetDrifTimeStats(self, relevant_events, cadence):
203 """Get the drift time statistics.
204
205 This method will calculate drift_time stats, that is to say :
206 drift_time::= list(actual render begin - ideal render).
207 rendering_length error::= the rendering length error.
208
209 Args:
210 relevant_events: events to get drift times stats from.
211 cadence: list of frame persistence values.
212
213 Returns:
214 a tuple of (drift_time, rendering_length_error).
215 """
216 drift_time = []
217 old_ideal_render = 0
218 discrepancy = []
219 index = 0
220 ideal_render_instant = 'Ideal Render Instant'
221 for event in relevant_events:
222 current_ideal_render = event.args[ideal_render_instant]
223 if current_ideal_render == old_ideal_render:
224 # Skip to next event because we're looking for a source frame.
225 continue
226 actual_render_begin = event.args['Actual Render Begin']
227 drift_time.append(actual_render_begin - current_ideal_render)
228 discrepancy.append(abs(current_ideal_render - old_ideal_render
229 - VSYNC_DURATION * cadence[index]))
230 old_ideal_render = current_ideal_render
231 index += 1
232 discrepancy.pop(0)
233 last_ideal_render = relevant_events[-1].args[ideal_render_instant]
234 first_ideal_render = relevant_events[0].args[ideal_render_instant]
235 rendering_length_error = 100.0 * (sum([x for x in discrepancy]) /
236 (last_ideal_render - first_ideal_render))
237
238 return drift_time, rendering_length_error
239
240 def _GetSmoothnessStats(self, norm_drift_time):
241 """Get the smoothness stats from the normalized drift time.
242
243 This method will calculate the smoothness score, along with the percentage
244 of frames badly out of sync and the percentage of frames out of sync. To be
245 considered badly out of sync, a frame has to have missed rendering by at
246 least 2*VSYNC_DURATION. To be considered out of sync, a frame has to have
247 missed rendering by at least one VSYNC_DURATION.
248 The smoothness score is a measure of how out of sync the frames are.
249
250 Args:
251 norm_drift_time: normalized drift time.
252
253 Returns:
254 a tuple of (percent_badly_oos, percent_out_of_sync, smoothness_score)
255 """
256 # How many times is a frame later/earlier than T=2*VSYNC_DURATION. Time is
257 # in microseconds.
258 frames_severely_out_of_sync = len(
259 [x for x in norm_drift_time if abs(x) > 2 * VSYNC_DURATION])
260 percent_badly_oos = (
261 100.0 * frames_severely_out_of_sync / len(norm_drift_time))
262
263 # How many times is a frame later/earlier than VSYNC_DURATION.
264 frames_out_of_sync = len(
265 [x for x in norm_drift_time if abs(x) > VSYNC_DURATION])
266 percent_out_of_sync = (
267 100.0 * frames_out_of_sync / len(norm_drift_time))
268
269 frames_oos_only_once = frames_out_of_sync - frames_severely_out_of_sync
270
271 # Calculate smoothness metric. From the formula, we can see that smoothness
272 # score can be negative.
273 smoothness_score = 100.0 - 100.0 * (frames_oos_only_once +
274 SEVERITY * frames_severely_out_of_sync) / len(norm_drift_time)
275
276 # Minimum smoothness_score value allowed is zero.
277 if smoothness_score < 0:
278 smoothness_score = 0
279
280 return (percent_badly_oos, percent_out_of_sync, smoothness_score)
281
282 def _GetFreezingScore(self, frame_distribution):
283 """Get the freezing score."""
284
285 # The freezing score is based on the source to output distribution.
286 number_vsyncs = sum([n * frame_distribution[n]
287 for n in frame_distribution])
288 frozen_frames = self._GetFrozenFramesReports(frame_distribution)
289
290 # Calculate freezing metric.
291 # Freezing metric can be negative if things are really bad. In that case we
292 # change it to zero as minimum valud.
293 freezing_score = 100.0
294 for frozen_report in frozen_frames:
295 weight = self._FrozenPenaltyWeight(frozen_report['frozen_frames'])
296 freezing_score -= (
297 100.0 * frozen_report['occurrences'] / number_vsyncs * weight)
298 if freezing_score < 0:
299 freezing_score = 0
300
301 return freezing_score
302
303 def GetTimeStats(self):
304 """Calculate time stamp stats for all remote stream events."""
305 stats = {}
306 for stream, relevant_events in self.stream_to_events.iteritems():
307 if len(relevant_events) == 1:
308 logging.debug('Found a stream=%s with just one event', stream)
309 continue
310 if not self._IsRemoteStream(stream):
311 logging.info('Skipping processing of local stream: %s', stream)
312 continue
313
314 cadence = self._GetCadence(relevant_events)
315 frame_distribution = self._GetSourceToOutputDistribution(cadence)
316 fps = self._GetFpsFromCadence(frame_distribution)
317
318 drift_time_stats = self._GetDrifTimeStats(relevant_events, cadence)
319 (drift_time, rendering_length_error) = drift_time_stats
320
321 # Drift time normalization.
322 mean_drift_time = numpy.mean(drift_time)
323 norm_drift_time = [abs(x - mean_drift_time) for x in drift_time]
324
325 smoothness_stats = self._GetSmoothnessStats(norm_drift_time)
326 (percent_badly_oos, percent_out_of_sync,
327 smoothness_score) = smoothness_stats
328
329 freezing_score = self._GetFreezingScore(frame_distribution)
330
331 stats = {
332 'drift_time': drift_time,
nednguyen 2015/09/28 20:56:43 I would prefer you create a light weight "TimeStat
cpaulin (no longer in chrome) 2015/09/29 15:50:25 Sure, opted for this class: class TimeStats(object
333 'mean_drift_time': mean_drift_time,
334 'std_dev_drift_time': numpy.std(drift_time),
335 'percent_badly_out_of_sync': percent_badly_oos,
336 'percent_out_of_sync': percent_out_of_sync,
337 'smoothness_score': smoothness_score,
338 'freezing_score': freezing_score,
339 'rendering_length_error': rendering_length_error,
340 'fps': fps,
341 'frame_distribution': frame_distribution}
342 print "Stats for remote stream {0}: {1}".format(stream, stats)
nednguyen 2015/09/28 20:56:43 Please remove the debug print
cpaulin (no longer in chrome) 2015/09/29 15:50:25 Sure
343 return stats
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698