Chromium Code Reviews| Index: tools/telemetry/telemetry/web_perf/metrics/webrtc_rendering_timeline.py |
| diff --git a/tools/telemetry/telemetry/web_perf/metrics/webrtc_rendering_timeline.py b/tools/telemetry/telemetry/web_perf/metrics/webrtc_rendering_timeline.py |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..8cf695569ebe432881301b65646d8491130aef15 |
| --- /dev/null |
| +++ b/tools/telemetry/telemetry/web_perf/metrics/webrtc_rendering_timeline.py |
| @@ -0,0 +1,351 @@ |
| +# Copyright 2015 The Chromium Authors. All rights reserved. |
| +# Use of this source code is governed by a BSD-style license that can be |
| +# found in the LICENSE file. |
| +import logging |
| + |
| +import numpy |
| + |
| +from telemetry.value import list_of_scalar_values |
| +from telemetry.value import list_of_string_values |
| +from telemetry.value import scalar |
| +from telemetry.web_perf.metrics import timeline_based_metric |
| + |
| +WEB_MEDIA_PLAYER_MS_EVENT = 'WebMediaPlayerMS::UpdateCurrentFrame' |
| + |
| +DISPLAY_HERTZ = 60.0 |
| +VSYNC_DURATION = 1e6 / DISPLAY_HERTZ |
| +# When to consider a frame frozen (in VSYNC units): |
| +# meaning 1 initial frame + 5 repeats of that frame. |
| +FROZEN_THRESHOLD = 6 |
| +# Severity factor. |
| +SEVERITY = 3 |
| + |
| + |
| +class WebMediaPlayerMsRenderingStats(object): |
|
phoglund_chromium
2015/09/17 11:23:58
Move this to its own file? Also I would love a uni
cpaulin (no longer in chrome)
2015/09/17 23:58:55
Done.
|
| + """Analyzes events of WebMediaPlayerMs type.""" |
| + |
| + def __init__(self, events): |
| + """Save relevant events.""" |
| + self.events = {} |
| + self.relevant_events = [] |
| + for event in events: |
|
phoglund_chromium
2015/09/17 11:23:58
This algorithm is a bit hard to read. So it's
for
cpaulin (no longer in chrome)
2015/09/17 23:58:55
Done.
|
| + if (event.args and 'Serial' in event.args |
| + and event.args['Serial'] in self.events): |
| + self.events[event.args['Serial']].append(event) |
| + elif event.args and 'Serial' in event.args: |
| + self.events[event.args['Serial']] = [event] |
| + |
| + def InferCadence(self): |
| + """Calculate the apparent cadence of the rendering.""" |
| + # Term 'cadence' loosely used here for lack of a better word. |
| + cadence = [] |
| + frame_persistence = 0 |
| + old_ideal_render = 0 |
| + for event in self.relevant_events: |
| + if (event.args and 'Ideal Render Instant' in event.args |
|
phoglund_chromium
2015/09/17 11:23:58
consider
ideal_render_instant = 'Ideal Render Ins
cpaulin (no longer in chrome)
2015/09/17 23:58:55
Acknowledged.
|
| + and event.args['Ideal Render Instant'] == old_ideal_render): |
| + frame_persistence += 1 |
| + elif event.args and 'Ideal Render Instant' in event.args: |
| + cadence.append(frame_persistence) |
| + frame_persistence = 1 |
| + old_ideal_render = event.args['Ideal Render Instant'] |
| + cadence.append(frame_persistence) |
| + cadence.pop(0) |
| + return cadence |
| + |
| + def Bucketize(self, cadence): |
|
phoglund_chromium
2015/09/17 11:23:58
"Bucketize" isn't a great name, what about Compute
|
| + """Create distribution for the cadence frame display values.""" |
| + # If the overall display distribution is A1:A2:..:An, |
| + # this will tell us how many times a frame |
| + # stays displayed during Ak vsync duration (i.e. Ak/DISPLAY_HERTZ) |
| + # also known as 'source to output' distribution. |
| + bucket = {} |
| + for ticks in cadence: |
| + if ticks in bucket: |
| + bucket[ticks] += 1 |
| + else: |
| + bucket[ticks] = 1 |
| + return bucket |
| + |
| + def InferFpsFromCadence(self, bucket): |
| + """Calculate the apparent FPS from cadence pattern.""" |
| + # The mean ratio is the barycenter |
| + number_frames = sum([bucket[ticks] for ticks in bucket]) |
| + number_vsyncs = sum([ticks * bucket[ticks] for ticks in bucket]) |
| + mean_ratio = float(number_vsyncs) / number_frames |
| + fps = DISPLAY_HERTZ / mean_ratio |
| + return fps |
| + |
| + def InferFrozenFramesEvents(self, bucket): |
| + """Find evidence of frozen frames in distribution.""" |
| + # For simplicity we count as freezing the frames |
| + # that appear at least five times in a row |
| + # counted from 'Ideal Render Instant' perspective. |
| + frozen_frames = [] |
| + for ticks in bucket: |
| + if ticks >= FROZEN_THRESHOLD: |
| + logging.error('%s frames not updated after %s vsyncs', |
| + bucket[ticks], ticks) |
| + frozen_frames.append( |
| + {'frozen_frames': ticks -1, |
| + 'occurences': bucket[ticks]}) |
| + return frozen_frames |
| + |
| + def FrozenPenaltyWeight(self, number_frozen_frames): |
| + """Returns the weighted penalty for a number of frozen frames.""" |
| + # As mentioned earlier, we count for frozen anything above 6 vsync |
| + # display duration for the same 'Initial Render Instant'. |
| + penalty = { |
| + 0: 0, |
| + 1: 0, |
| + 2: 0, |
| + 3: 0, |
| + 4: 0, |
| + 5: 1, |
| + 6: 5, |
| + 7: 15, |
| + 8: 25 |
| + } |
| + weight = penalty.get(number_frozen_frames, |
| + 8 * (number_frozen_frames - 4)) |
| + return weight |
| + |
| + def IsRemoteStream(self, stream): |
| + """Determines if a stream is remote or local.""" |
| + return stream[0] == '1' |
| + |
| + |
| + def InferTimeStats(self): |
|
phoglund_chromium
2015/09/17 11:23:58
You're going to have to rewrite this one quite a b
cpaulin (no longer in chrome)
2015/09/17 23:58:55
Acknowledged.
|
| + """Calculate time stamp stats for all events.""" |
| + stats = {} |
| + for stream in self.events: |
| + self.relevant_events = self.events[stream] |
|
phoglund_chromium
2015/09/17 11:23:58
Ugh, don't do this - get rid of self.relevant.even
cpaulin (no longer in chrome)
2015/09/17 23:58:55
Acknowledged.
|
| + if not self.IsRemoteStream(stream): |
| + logging.info('Skipping processing of local stream: %s', stream) |
| + continue |
| + cadence = self.InferCadence() |
| + bucket = self.Bucketize(cadence) |
| + fps = self.InferFpsFromCadence(bucket) |
| + frozen_frames = self.InferFrozenFramesEvents(bucket) |
| + # Drift time between Ideal Render Instant and Actual Render Begin. |
|
phoglund_chromium
2015/09/17 11:23:58
The method really gets off to a good start, with c
|
| + drift_time = [] |
| + old_ideal_render = 0 |
| + discrepancy = [] |
| + index = 0 |
| + for event in self.relevant_events: |
| + current_ideal_render = event.args['Ideal Render Instant'] |
| + if current_ideal_render == old_ideal_render: |
| + continue |
| + drift_time.append( |
| + event.args['Actual Render Begin'] - current_ideal_render) |
| + discrepancy.append(abs(current_ideal_render - old_ideal_render |
| + - VSYNC_DURATION * cadence[index])) |
| + old_ideal_render = current_ideal_render |
| + index += 1 |
| + discrepancy.pop(0) |
| + last_ideal_render = self.relevant_events[-1].args['Ideal Render Instant'] |
| + first_ideal_render = self.relevant_events[0].args['Ideal Render Instant'] |
| + if last_ideal_render == first_ideal_render: |
| + logging.error('Found a stream=%s with just one event', stream) |
| + continue |
| + rendering_length_error = 100.0 * (sum([x for x in discrepancy]) / |
| + (last_ideal_render - first_ideal_render)) |
| + # Some stats on drift time. |
| + mean_drift_time = numpy.mean(drift_time) |
| + std_dev_drift_time = numpy.std(drift_time) |
| + norm_drift_time = [abs(x - mean_drift_time) for x in drift_time] |
| + # How many times is a frame later/earlier than T=2/DISPLAY_HERTZ. |
| + # Time is in microseconds. |
| + frames_severely_out_of_sync = len( |
| + [x for x in norm_drift_time if abs(x) > 2 * VSYNC_DURATION]) |
| + percent_badly_oos = ( |
| + 100.0 * frames_severely_out_of_sync / len(norm_drift_time)) |
| + # How many times is a frame later/earlier than 1/DISPLAY_HERTZ. |
| + frames_out_of_sync = len( |
| + [x for x in norm_drift_time if abs(x) > VSYNC_DURATION]) |
| + percent_out_of_sync = ( |
| + 100.0 * frames_out_of_sync / len(norm_drift_time)) |
| + |
| + frames_oos_only_once = frames_out_of_sync - frames_severely_out_of_sync |
| + # For safety I don't use number_vsyncs = len(self.relevant_events) just |
| + # in case other events are added later. |
| + number_vsyncs = sum([n * bucket[n] for n in bucket]) |
| + # Calculate smoothness metric. |
| + # From the formula, we can see that smoothness score can be negative. |
| + smoothness_score = 100.0 - 100.0*(frames_oos_only_once + |
| + SEVERITY * frames_severely_out_of_sync) / len(norm_drift_time) |
| + # Calculate freezing metric. |
| + # Freezing metric can be negative if things are really bad. |
| + freezing_score = 100.0 |
| + for frozen_report in frozen_frames: |
| + weight = self.FrozenPenaltyWeight(frozen_report['frozen_frames']) |
| + freezing_score -= ( |
| + 100.0 * frozen_report['occurences'] / number_vsyncs * weight) |
| + # negative scores are meaningless, so zero them |
| + if freezing_score < 0: |
| + freezing_score = 0 |
| + if smoothness_score < 0: |
| + smoothness_score = 0 |
| + |
|
phoglund_chromium
2015/09/17 11:23:58
Here is how I would ideally want this method to lo
cpaulin (no longer in chrome)
2015/09/17 23:58:55
Patrik, I have grouped the stats by logical units:
|
| + stats = { |
| + 'drift_time': drift_time, |
| + 'mean_drift_time': mean_drift_time, |
| + 'std_dev_drift_time': std_dev_drift_time, |
| + 'percent_badly_out_of_sync': percent_badly_oos, |
| + 'percent_out_of_sync': percent_out_of_sync, |
| + 'smoothness_score': smoothness_score, |
| + 'freezing_score': freezing_score, |
| + 'rendering_length_error': rendering_length_error, |
| + 'fps': fps, |
| + 'bucket': bucket} |
| + print "Stats for remote stream {0}: {1}".format(stream, stats) |
| + return stats |
| + |
| + |
| +class WebRtcRenderingTimelineMetric(timeline_based_metric.TimelineBasedMetric): |
| + """WebrtcRenderingTimelineMetric calculates metric for WebMediaPlayerMS. |
| + |
| + The following metrics are added to the results: |
| + WebRtcRendering_drift_time usec |
| + WebRTCRendering_std_dev_drift_time usec |
| + WebRTCRendering_percent_badly_out_of_sync % |
| + WebRTCRendering_percent_out_of_sync % |
| + WebRTCRendering_fps FPS |
| + WebRTCRendering_smoothness_score % |
| + WebRTCRendering_freezing_score % |
| + WebRTCRendering_rendering_length_error % |
| + """ |
| + |
| + def __init__(self): |
| + super(WebRtcRenderingTimelineMetric, self).__init__() |
| + |
| + @staticmethod |
| + def IsMediaPlayerMSEvent(event): |
| + """Verify that the event is a webmediaplayerMS event.""" |
| + return event.name == WEB_MEDIA_PLAYER_MS_EVENT |
| + |
| + @staticmethod |
| + def IsEventInInteraction(event, interaction): |
| + """Verify that the event belong to the gbiven interaction.""" |
| + return interaction[0].start <= event.start <= interaction[0].end |
| + |
| + def AddResults(self, model, renderer_thread, interactions, results): |
| + """Adding metrics to the results.""" |
| + assert interactions |
| + found_events = [] |
| + for event in renderer_thread.parent.IterAllEvents( |
| + event_predicate=self.IsMediaPlayerMSEvent): |
| + if self.IsEventInInteraction(event, interactions): |
| + found_events.append(event) |
| + stats_parser = WebMediaPlayerMsRenderingStats(found_events) |
| + rendering_stats = stats_parser.InferTimeStats() |
| + none_reason = None |
| + if not rendering_stats: |
| + rendering_stats = dict.fromkeys([ |
| + 'drift_time' |
| + 'mean_drift_time', |
| + 'std_dev_drift_time', |
| + 'percent_badly_out_of_sync', |
| + 'percent_out_of_sync', |
| + 'smoothness_score', |
| + 'freezing_score', |
| + 'rendering_length_error', |
| + 'fps', |
| + 'bucket']) |
| + none_reason = "No WebMediaPlayerMS::UpdateCurrentFrame event found" |
| + logging.info('rendering stats : %s', rendering_stats) |
| + results.AddValue(list_of_scalar_values.ListOfScalarValues( |
| + results.current_page, |
| + 'WebRtcRendering_drift_time', |
| + 'usec', |
| + rendering_stats['drift_time'], |
| + important=True, |
| + description='Drift time for a rendered frame', |
| + none_value_reason=none_reason)) |
| + |
| + results.AddValue(scalar.ScalarValue( |
| + results.current_page, |
| + 'WebRTCRendering_mean_drift_time', |
| + 'usec', |
| + rendering_stats['mean_drift_time'], |
| + important=True, |
| + description='Mean drift time for frames', |
| + none_value_reason=none_reason)) |
| + |
| + results.AddValue(scalar.ScalarValue( |
| + results.current_page, |
| + 'WebRTCRendering_std_dev_drift_time', |
| + 'usec', |
| + rendering_stats['std_dev_drift_time'], |
| + important=True, |
| + description='Standard deviation of drift time for frames', |
| + none_value_reason=none_reason)) |
| + |
| + results.AddValue(scalar.ScalarValue( |
| + results.current_page, |
| + 'WebRTCRendering_percent_badly_out_of_sync', |
| + '%', |
| + rendering_stats['percent_badly_out_of_sync'], |
| + important=True, |
| + description='Percentage of frame which drifted more than 2 VSYNC', |
| + none_value_reason=none_reason)) |
| + |
| + results.AddValue(scalar.ScalarValue( |
| + results.current_page, |
| + 'WebRTCRendering_percent_out_of_sync', |
| + '%', |
| + rendering_stats['percent_out_of_sync'], |
| + important=True, |
| + description='Percentage of frame which drifted more than 1 VSYNC', |
| + none_value_reason=none_reason)) |
| + |
| + # make the output distribution a list since |
| + # no facilities for dict values exist (yet) |
| + bucket_list = [] |
| + for key, value in rendering_stats['bucket'].iteritems(): |
| + temp = '%s:%s' % (key, value) |
| + bucket_list.append(temp) |
| + results.AddValue(list_of_string_values.ListOfStringValues( |
| + results.current_page, |
| + 'WebRtcRendering_bucket', |
| + 'frames:occurences', |
| + bucket_list, |
| + important=True, |
| + description='Output distribution of frames', |
| + none_value_reason=none_reason)) |
| + |
| + results.AddValue(scalar.ScalarValue( |
| + results.current_page, |
| + 'WebRTCRendering_fps', |
| + 'FPS', |
| + rendering_stats['fps'], |
| + important=True, |
| + description='Calculated Frame Rate of video rendering', |
| + none_value_reason=none_reason)) |
| + |
| + results.AddValue(scalar.ScalarValue( |
| + results.current_page, |
| + 'WebRTCRendering_smoothness_score', |
| + '%', |
| + rendering_stats['smoothness_score'], |
| + important=True, |
| + description='Smoothness score of rendering', |
| + none_value_reason=none_reason)) |
| + |
| + results.AddValue(scalar.ScalarValue( |
| + results.current_page, |
| + 'WebRTCRendering_freezing_score', |
| + '%', |
| + rendering_stats['freezing_score'], |
| + important=True, |
| + description='Freezing score of rendering', |
| + none_value_reason=none_reason)) |
| + |
| + results.AddValue(scalar.ScalarValue( |
| + results.current_page, |
| + 'WebRTCRendering_rendering_length_error', |
| + '%', |
| + rendering_stats['rendering_length_error'], |
| + important=True, |
| + description='Rendering length error rate', |
| + none_value_reason=none_reason)) |