Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 # Copyright 2016 The Chromium Authors. All rights reserved. | 1 # Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 """Identifies key events related to user satisfaction. | 5 """Identifies key events related to user satisfaction. |
| 6 | 6 |
| 7 Several lenses are defined, for example FirstTextPaintLens and | 7 Several lenses are defined, for example FirstTextPaintLens and |
| 8 FirstSignificantPaintLens. | 8 FirstSignificantPaintLens. |
| 9 | |
| 10 When run from the command line, takes a lens name and a trace, and prints the | |
| 11 fingerprints of the critical resources to stdout. | |
| 9 """ | 12 """ |
| 10 import logging | 13 import logging |
| 11 import operator | 14 import operator |
| 12 | 15 |
| 13 import common_util | 16 import common_util |
| 14 | 17 |
| 15 | 18 |
| 16 class _UserSatisfiedLens(object): | 19 class _UserSatisfiedLens(object): |
| 17 """A base class for all user satisfaction metrics. | 20 """A base class for all user satisfaction metrics. |
| 18 | 21 |
| 19 All of these work by identifying a user satisfaction event from the trace, and | 22 All of these work by identifying a user satisfaction event from the trace, and |
| 20 then building a set of request ids whose loading is needed to achieve that | 23 then building a set of request ids whose loading is needed to achieve that |
| 21 event. Subclasses need only provide the time computation. The base class will | 24 event. Subclasses need only provide the time computation. The base class will |
| 22 use that to construct the request ids. | 25 use that to construct the request ids. |
| 23 """ | 26 """ |
| 24 _ATTRS = ['_satisfied_msec', '_event_msec', '_postload_msec', | 27 _ATTRS = ['_satisfied_msec', '_event_msec', '_postload_msec', |
| 25 '_critical_request_ids'] | 28 '_critical_request_ids'] |
| 26 | 29 |
| 30 def CriticalRequests(self): | |
| 31 """Critical requests. | |
| 32 | |
| 33 Returns: | |
| 34 A sequence of request_track.Request objects representing an estimate of | |
| 35 all requests that are necessary for the user satisfaction defined by this | |
| 36 class. | |
| 37 """ | |
| 38 raise NotImplementedError | |
| 39 | |
| 40 def CriticalRequestIds(self): | |
| 41 """Ids of critical requests.""" | |
| 42 return set(rq.request_id for rq in self.CriticalRequests()) | |
| 43 | |
| 44 def CriticalFingerprints(self): | |
| 45 """Fingerprints of critical requests.""" | |
| 46 return set(rq.fingerprint for rq in self.CriticalRequests()) | |
| 47 | |
| 48 def PostloadTimeMsec(self): | |
| 49 """Return postload time. | |
| 50 | |
| 51 The postload time is an estimate of the amount of time needed by chrome to | |
| 52 transform the critical results into the satisfying event. | |
| 53 | |
| 54 Returns: | |
| 55 Postload time in milliseconds. | |
| 56 """ | |
| 57 return 0 | |
| 58 | |
| 59 | |
| 60 class RequestFingerprintLens(_UserSatisfiedLens): | |
| 61 """A lens built using requests in a trace that match a set of fingerprints.""" | |
| 62 def __init__(self, trace, fingerprints): | |
| 63 fingerprints = set(fingerprints) | |
| 64 self._critical_requests = [rq for rq in trace.request_track.GetEvents() | |
|
Benoit L
2016/04/27 12:40:50
The fingerprint is constructed from the URL. What
mattcary
2016/04/27 12:47:55
It's counted as the same request. I wasn't sure wh
| |
| 65 if rq.fingerprint in fingerprints] | |
| 66 | |
| 67 def CriticalRequests(self): | |
| 68 """Ids of critical requests.""" | |
| 69 return set(self._critical_requests) | |
| 70 | |
| 71 | |
| 72 class _FirstEventLens(_UserSatisfiedLens): | |
| 73 """Helper abstract subclass that defines users first event manipulations.""" | |
| 74 # pylint can't handle abstract subclasses. | |
| 75 # pylint: disable=abstract-method | |
| 76 | |
| 27 def __init__(self, trace): | 77 def __init__(self, trace): |
| 28 """Initialize the lens. | 78 """Initialize the lens. |
| 29 | 79 |
| 30 Args: | 80 Args: |
| 31 trace: (LoadingTrace) the trace to use in the analysis. | 81 trace: (LoadingTrace) the trace to use in the analysis. |
| 32 """ | 82 """ |
| 33 self._satisfied_msec = None | 83 self._satisfied_msec = None |
| 34 self._event_msec = None | 84 self._event_msec = None |
| 35 self._postload_msec = None | 85 self._postload_msec = None |
| 36 self._critical_request_ids = None | 86 self._critical_request_ids = None |
| 37 if trace is None: | 87 if trace is None: |
| 38 return | 88 return |
| 39 self._CalculateTimes(trace.tracing_track) | 89 self._CalculateTimes(trace) |
| 40 critical_requests = self._RequestsBefore( | 90 self._critical_requests = self._RequestsBefore( |
| 41 trace.request_track, self._satisfied_msec) | 91 trace.request_track, self._satisfied_msec) |
| 42 self._critical_request_ids = set(rq.request_id for rq in critical_requests) | 92 self._critical_request_ids = set(rq.request_id |
| 43 if critical_requests: | 93 for rq in self._critical_requests) |
| 44 last_load = max(rq.end_msec for rq in critical_requests) | 94 if self._critical_requests: |
| 95 last_load = max(rq.end_msec for rq in self._critical_requests) | |
| 45 else: | 96 else: |
| 46 last_load = float('inf') | 97 last_load = float('inf') |
| 47 self._postload_msec = self._event_msec - last_load | 98 self._postload_msec = self._event_msec - last_load |
| 48 | 99 |
| 49 def CriticalRequests(self): | 100 def CriticalRequests(self): |
| 50 """Request ids of critical requests. | 101 """Override.""" |
| 51 | 102 return self._critical_requests |
| 52 Returns: | |
| 53 A set of request ids (as strings) of an estimate of all requests that are | |
| 54 necessary for the user satisfaction defined by this class. | |
| 55 """ | |
| 56 return self._critical_request_ids | |
| 57 | 103 |
| 58 def PostloadTimeMsec(self): | 104 def PostloadTimeMsec(self): |
| 59 """Return postload time. | 105 """Override.""" |
| 60 | |
| 61 The postload time is an estimate of the amount of time needed by chrome to | |
| 62 transform the critical results into the satisfying event. | |
| 63 | |
| 64 Returns: | |
| 65 Postload time in milliseconds. | |
| 66 """ | |
| 67 return self._postload_msec | 106 return self._postload_msec |
| 68 | 107 |
| 69 def ToJsonDict(self): | 108 def ToJsonDict(self): |
| 70 return common_util.SerializeAttributesToJsonDict({}, self, self._ATTRS) | 109 return common_util.SerializeAttributesToJsonDict({}, self, self._ATTRS) |
| 71 | 110 |
| 72 @classmethod | 111 @classmethod |
| 73 def FromJsonDict(cls, json_dict): | 112 def FromJsonDict(cls, json_dict): |
| 74 result = cls(None) | 113 result = cls(None) |
| 75 return common_util.DeserializeAttributesFromJsonDict( | 114 return common_util.DeserializeAttributesFromJsonDict( |
| 76 json_dict, result, cls._ATTRS) | 115 json_dict, result, cls._ATTRS) |
| 77 | 116 |
| 78 def _CalculateTimes(self, tracing_track): | 117 def _CalculateTimes(self, trace): |
| 79 """Subclasses should implement to set _satisfied_msec and _event_msec.""" | 118 """Subclasses should implement to set _satisfied_msec and _event_msec.""" |
| 80 raise NotImplementedError | 119 raise NotImplementedError |
| 81 | 120 |
| 82 @classmethod | 121 @classmethod |
| 83 def _RequestsBefore(cls, request_track, time_ms): | 122 def _RequestsBefore(cls, request_track, time_ms): |
| 84 return [rq for rq in request_track.GetEvents() | 123 return [rq for rq in request_track.GetEvents() |
| 85 if rq.end_msec <= time_ms] | 124 if rq.end_msec <= time_ms] |
| 86 | 125 |
| 87 | |
| 88 class _FirstEventLens(_UserSatisfiedLens): | |
| 89 """Helper abstract subclass that defines users first event manipulations.""" | |
| 90 # pylint can't handle abstract subclasses. | |
| 91 # pylint: disable=abstract-method | |
| 92 | |
| 93 @classmethod | 126 @classmethod |
| 94 def _ExtractFirstTiming(cls, times): | 127 def _ExtractBestTiming(cls, times): |
| 95 if not times: | 128 if not times: |
| 96 return float('inf') | 129 return float('inf') |
| 97 if len(times) != 1: | 130 assert len(times) == 1, \ |
| 98 # TODO(mattcary): in some cases a trace has two first paint events. Why? | 131 'Unexpected duplicate {}: {} with spread of {}'.format( |
| 99 logging.error('%d %s with spread of %s', len(times), | 132 str(cls), len(times), max(times) - min(times)) |
| 100 str(cls), max(times) - min(times)) | 133 return float(max(times)) |
| 101 return float(min(times)) | |
| 102 | 134 |
| 103 | 135 |
| 104 class FirstTextPaintLens(_FirstEventLens): | 136 class FirstTextPaintLens(_FirstEventLens): |
| 105 """Define satisfaction by the first text paint. | 137 """Define satisfaction by the first text paint. |
| 106 | 138 |
| 107 This event is taken directly from a trace. | 139 This event is taken directly from a trace. |
| 108 """ | 140 """ |
| 109 def _CalculateTimes(self, tracing_track): | 141 def _CalculateTimes(self, trace): |
| 110 first_paints = [e.start_msec for e in tracing_track.GetEvents() | 142 first_paints = [ |
| 111 if e.Matches('blink.user_timing', 'firstPaint')] | 143 e.start_msec for e in trace.tracing_track.GetMatchingMainFrameEvents( |
| 144 'blink.user_timing', 'firstPaint')] | |
| 112 self._satisfied_msec = self._event_msec = \ | 145 self._satisfied_msec = self._event_msec = \ |
| 113 self._ExtractFirstTiming(first_paints) | 146 self._ExtractBestTiming(first_paints) |
| 114 | 147 |
| 115 | 148 |
| 116 class FirstContentfulPaintLens(_FirstEventLens): | 149 class FirstContentfulPaintLens(_FirstEventLens): |
| 117 """Define satisfaction by the first contentful paint. | 150 """Define satisfaction by the first contentful paint. |
| 118 | 151 |
| 119 This event is taken directly from a trace. Internally to chrome it's computed | 152 This event is taken directly from a trace. Internally to chrome it's computed |
| 120 by filtering out things like background paint from firstPaint. | 153 by filtering out things like background paint from firstPaint. |
| 121 """ | 154 """ |
| 122 def _CalculateTimes(self, tracing_track): | 155 def _CalculateTimes(self, trace): |
| 123 first_paints = [e.start_msec for e in tracing_track.GetEvents() | 156 first_paints = [ |
| 124 if e.Matches('blink.user_timing', 'firstContentfulPaint')] | 157 e.start_msec for e in trace.tracing_track.GetMatchingMainFrameEvents( |
| 158 'blink.user_timing', 'firstContentfulPaint')] | |
| 125 self._satisfied_msec = self._event_msec = \ | 159 self._satisfied_msec = self._event_msec = \ |
| 126 self._ExtractFirstTiming(first_paints) | 160 self._ExtractBestTiming(first_paints) |
| 127 | 161 |
| 128 | 162 |
| 129 class FirstSignificantPaintLens(_FirstEventLens): | 163 class FirstSignificantPaintLens(_FirstEventLens): |
| 130 """Define satisfaction by the first paint after a big layout change. | 164 """Define satisfaction by the first paint after a big layout change. |
| 131 | 165 |
| 132 Our satisfaction time is that of the layout change, as all resources must have | 166 Our satisfaction time is that of the layout change, as all resources must have |
| 133 been loaded to compute the layout. Our event time is that of the next paint as | 167 been loaded to compute the layout. Our event time is that of the next paint as |
| 134 that is the observable event. | 168 that is the observable event. |
| 135 """ | 169 """ |
| 136 FIRST_LAYOUT_COUNTER = 'LayoutObjectsThatHadNeverHadLayout' | 170 FIRST_LAYOUT_COUNTER = 'LayoutObjectsThatHadNeverHadLayout' |
| 137 | 171 |
| 138 def _CalculateTimes(self, tracing_track): | 172 def _CalculateTimes(self, trace): |
| 139 sync_paint_times = [] | 173 sync_paint_times = [] |
| 140 layouts = [] # (layout item count, msec). | 174 layouts = [] # (layout item count, msec). |
| 141 for e in tracing_track.GetEvents(): | 175 for e in trace.tracing_track.GetEvents(): |
| 176 if ('frame' in e.args and | |
| 177 e.args['frame'] != trace.tracing_track.GetMainFrameID()): | |
| 178 continue | |
| 179 # If we don't know have a frame id, we assume it applies to all events. | |
| 180 | |
| 142 # TODO(mattcary): is this the right paint event? Check if synchronized | 181 # TODO(mattcary): is this the right paint event? Check if synchronized |
| 143 # paints appear at the same time as the first*Paint events, above. | 182 # paints appear at the same time as the first*Paint events, above. |
| 144 if e.Matches('blink', 'FrameView::SynchronizedPaint'): | 183 if e.Matches('blink', 'FrameView::SynchronizedPaint'): |
| 145 sync_paint_times.append(e.start_msec) | 184 sync_paint_times.append(e.start_msec) |
| 146 if ('counters' in e.args and | 185 if ('counters' in e.args and |
| 147 self.FIRST_LAYOUT_COUNTER in e.args['counters']): | 186 self.FIRST_LAYOUT_COUNTER in e.args['counters']): |
| 148 layouts.append((e.args['counters'][self.FIRST_LAYOUT_COUNTER], | 187 layouts.append((e.args['counters'][self.FIRST_LAYOUT_COUNTER], |
| 149 e.start_msec)) | 188 e.start_msec)) |
| 150 assert layouts, ('No layout events, was the disabled-by-default-blink' | 189 assert layouts, ('No layout events, was the disabled-by-default-blink' |
| 151 '.debug.layout category enabled?') | 190 '.debug.layout category enabled?') |
| 191 assert sync_paint_times, \ | |
| 192 'No sync paint times, was the blink category disabled?' | |
| 152 layouts.sort(key=operator.itemgetter(0), reverse=True) | 193 layouts.sort(key=operator.itemgetter(0), reverse=True) |
| 153 self._satisfied_msec = layouts[0][1] | 194 self._satisfied_msec = layouts[0][1] |
| 154 self._event_msec = self._ExtractFirstTiming([ | 195 self._event_msec = min(t for t in sync_paint_times |
| 155 min(t for t in sync_paint_times if t > self._satisfied_msec)]) | 196 if t > self._satisfied_msec) |
| 197 | |
| 198 | |
| 199 def main(lens_name, trace_file): | |
| 200 assert (lens_name in globals() and | |
| 201 not lens_name.startswith('_') and | |
| 202 lens_name.endswith('Lens')), 'Bad lens %s' % lens_name | |
| 203 lens_cls = globals()[lens_name] | |
| 204 trace = loading_trace.LoadingTrace.FromJsonFile(trace_file) | |
| 205 lens = lens_cls(trace) | |
| 206 for fp in sorted(lens.CriticalFingerprints()): | |
| 207 print fp | |
| 208 | |
| 209 | |
| 210 if __name__ == '__main__': | |
| 211 import sys | |
| 212 import loading_trace | |
| 213 main(sys.argv[1], sys.argv[2]) | |
| OLD | NEW |