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() |
| 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 _CheckCategory(cls, tracing_track, category): | 127 def _CheckCategory(cls, tracing_track, category): |
95 assert category in tracing_track.Categories(), ( | 128 assert category in tracing_track.Categories(), ( |
96 'The "%s" category must be enabled.' % category) | 129 'The "%s" category must be enabled.' % category) |
97 | 130 |
98 @classmethod | 131 @classmethod |
99 def _ExtractFirstTiming(cls, times): | 132 def _ExtractBestTiming(cls, times): |
100 if not times: | 133 if not times: |
101 return float('inf') | 134 return float('inf') |
102 if len(times) != 1: | 135 assert len(times) == 1, \ |
103 # TODO(mattcary): in some cases a trace has two first paint events. Why? | 136 'Unexpected duplicate {}: {} with spread of {}'.format( |
104 logging.error('%d %s with spread of %s', len(times), | 137 str(cls), len(times), max(times) - min(times)) |
105 str(cls), max(times) - min(times)) | 138 return float(max(times)) |
106 return float(min(times)) | |
107 | 139 |
108 | 140 |
109 class FirstTextPaintLens(_FirstEventLens): | 141 class FirstTextPaintLens(_FirstEventLens): |
110 """Define satisfaction by the first text paint. | 142 """Define satisfaction by the first text paint. |
111 | 143 |
112 This event is taken directly from a trace. | 144 This event is taken directly from a trace. |
113 """ | 145 """ |
114 _EVENT_CATEGORY = 'blink.user_timing' | 146 _EVENT_CATEGORY = 'blink.user_timing' |
115 def _CalculateTimes(self, tracing_track): | 147 def _CalculateTimes(self, trace): |
116 self._CheckCategory(tracing_track, self._EVENT_CATEGORY) | 148 self._CheckCategory(trace.tracing_track, self._EVENT_CATEGORY) |
117 first_paints = [e.start_msec for e in tracing_track.GetEvents() | 149 first_paints = [ |
118 if e.Matches(self._EVENT_CATEGORY, 'firstPaint')] | 150 e.start_msec for e in trace.tracing_track.GetMatchingMainFrameEvents( |
| 151 'blink.user_timing', 'firstPaint')] |
119 self._satisfied_msec = self._event_msec = \ | 152 self._satisfied_msec = self._event_msec = \ |
120 self._ExtractFirstTiming(first_paints) | 153 self._ExtractBestTiming(first_paints) |
121 | 154 |
122 | 155 |
123 class FirstContentfulPaintLens(_FirstEventLens): | 156 class FirstContentfulPaintLens(_FirstEventLens): |
124 """Define satisfaction by the first contentful paint. | 157 """Define satisfaction by the first contentful paint. |
125 | 158 |
126 This event is taken directly from a trace. Internally to chrome it's computed | 159 This event is taken directly from a trace. Internally to chrome it's computed |
127 by filtering out things like background paint from firstPaint. | 160 by filtering out things like background paint from firstPaint. |
128 """ | 161 """ |
129 _EVENT_CATEGORY = 'blink.user_timing' | 162 _EVENT_CATEGORY = 'blink.user_timing' |
130 def _CalculateTimes(self, tracing_track): | 163 def _CalculateTimes(self, trace): |
131 self._CheckCategory(tracing_track, self._EVENT_CATEGORY) | 164 self._CheckCategory(trace.tracing_track, self._EVENT_CATEGORY) |
132 first_paints = [e.start_msec for e in tracing_track.GetEvents() | 165 first_paints = [ |
133 if e.Matches(self._EVENT_CATEGORY, 'firstContentfulPaint')] | 166 e.start_msec for e in trace.tracing_track.GetMatchingMainFrameEvents( |
| 167 'blink.user_timing', 'firstContentfulPaint')] |
134 self._satisfied_msec = self._event_msec = \ | 168 self._satisfied_msec = self._event_msec = \ |
135 self._ExtractFirstTiming(first_paints) | 169 self._ExtractBestTiming(first_paints) |
136 | 170 |
137 | 171 |
138 class FirstSignificantPaintLens(_FirstEventLens): | 172 class FirstSignificantPaintLens(_FirstEventLens): |
139 """Define satisfaction by the first paint after a big layout change. | 173 """Define satisfaction by the first paint after a big layout change. |
140 | 174 |
141 Our satisfaction time is that of the layout change, as all resources must have | 175 Our satisfaction time is that of the layout change, as all resources must have |
142 been loaded to compute the layout. Our event time is that of the next paint as | 176 been loaded to compute the layout. Our event time is that of the next paint as |
143 that is the observable event. | 177 that is the observable event. |
144 """ | 178 """ |
145 _FIRST_LAYOUT_COUNTER = 'LayoutObjectsThatHadNeverHadLayout' | 179 _FIRST_LAYOUT_COUNTER = 'LayoutObjectsThatHadNeverHadLayout' |
146 _EVENT_CATEGORY = 'disabled-by-default-blink.debug.layout' | 180 _EVENT_CATEGORIES = ['blink', 'disabled-by-default-blink.debug.layout'] |
147 def _CalculateTimes(self, tracing_track): | 181 def _CalculateTimes(self, trace): |
148 self._CheckCategory(tracing_track, self._EVENT_CATEGORY) | 182 for cat in self._EVENT_CATEGORIES: |
| 183 self._CheckCategory(trace.tracing_track, cat) |
149 sync_paint_times = [] | 184 sync_paint_times = [] |
150 layouts = [] # (layout item count, msec). | 185 layouts = [] # (layout item count, msec). |
151 for e in tracing_track.GetEvents(): | 186 for e in trace.tracing_track.GetEvents(): |
| 187 if ('frame' in e.args and |
| 188 e.args['frame'] != trace.tracing_track.GetMainFrameID()): |
| 189 continue |
| 190 # If we don't know have a frame id, we assume it applies to all events. |
| 191 |
152 # TODO(mattcary): is this the right paint event? Check if synchronized | 192 # TODO(mattcary): is this the right paint event? Check if synchronized |
153 # paints appear at the same time as the first*Paint events, above. | 193 # paints appear at the same time as the first*Paint events, above. |
154 if e.Matches('blink', 'FrameView::synchronizedPaint'): | 194 if e.Matches('blink', 'FrameView::synchronizedPaint'): |
155 sync_paint_times.append(e.start_msec) | 195 sync_paint_times.append(e.start_msec) |
156 if ('counters' in e.args and | 196 if ('counters' in e.args and |
157 self._FIRST_LAYOUT_COUNTER in e.args['counters']): | 197 self._FIRST_LAYOUT_COUNTER in e.args['counters']): |
158 layouts.append((e.args['counters'][self._FIRST_LAYOUT_COUNTER], | 198 layouts.append((e.args['counters'][self._FIRST_LAYOUT_COUNTER], |
159 e.start_msec)) | 199 e.start_msec)) |
160 assert layouts, 'No layout events' | 200 assert layouts, 'No layout events' |
| 201 assert sync_paint_times,'No sync paint times' |
161 layouts.sort(key=operator.itemgetter(0), reverse=True) | 202 layouts.sort(key=operator.itemgetter(0), reverse=True) |
162 self._satisfied_msec = layouts[0][1] | 203 self._satisfied_msec = layouts[0][1] |
163 self._event_msec = self._ExtractFirstTiming([ | 204 self._event_msec = min(t for t in sync_paint_times |
164 min(t for t in sync_paint_times if t > self._satisfied_msec)]) | 205 if t > self._satisfied_msec) |
| 206 |
| 207 |
| 208 def main(lens_name, trace_file): |
| 209 assert (lens_name in globals() and |
| 210 not lens_name.startswith('_') and |
| 211 lens_name.endswith('Lens')), 'Bad lens %s' % lens_name |
| 212 lens_cls = globals()[lens_name] |
| 213 trace = loading_trace.LoadingTrace.FromJsonFile(trace_file) |
| 214 lens = lens_cls(trace) |
| 215 for fp in sorted(lens.CriticalFingerprints()): |
| 216 print fp |
| 217 |
| 218 |
| 219 if __name__ == '__main__': |
| 220 import sys |
| 221 import loading_trace |
| 222 main(sys.argv[1], sys.argv[2]) |
OLD | NEW |