Chromium Code Reviews| 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 | |
| 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 | |
| OLD | NEW |