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 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 |