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 |