1/*
2 * Copyright (C) 2013 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 *    notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 *    notice, this list of conditions and the following disclaimer in the
11 *    documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26WebInspector.TimelineManager = function()
27{
28    WebInspector.Object.call(this);
29
30    WebInspector.Frame.addEventListener(WebInspector.Frame.Event.ProvisionalLoadStarted, this._startAutoCapturing, this);
31    WebInspector.Frame.addEventListener(WebInspector.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
32    WebInspector.Frame.addEventListener(WebInspector.Frame.Event.ResourceWasAdded, this._resourceWasAdded, this);
33
34    this._activeRecording = new WebInspector.TimelineRecording;
35    this._isCapturing = false;
36
37    this._boundStopCapturing = this.stopCapturing.bind(this);
38};
39
40WebInspector.TimelineManager.Event = {
41    CapturingStarted: "timeline-manager-capturing-started",
42    CapturingStopped: "timeline-manager-capturing-stopped"
43};
44
45WebInspector.TimelineManager.MaximumAutoRecordDuration = 90000; // 90 seconds
46WebInspector.TimelineManager.MaximumAutoRecordDurationAfterLoadEvent = 10000; // 10 seconds
47WebInspector.TimelineManager.DeadTimeRequiredToStopAutoRecordingEarly = 2000; // 2 seconds
48
49WebInspector.TimelineManager.prototype = {
50    constructor: WebInspector.TimelineManager,
51
52    // Public
53
54    get activeRecording()
55    {
56        return this._activeRecording;
57    },
58
59    isCapturing: function()
60    {
61        return this._isCapturing;
62    },
63
64    startCapturing: function()
65    {
66        TimelineAgent.start();
67
68        // COMPATIBILITY (iOS 7): recordingStarted event did not exist yet. Start explicitly.
69        if (!TimelineAgent.hasEvent("recordingStarted"))
70            this.capturingStarted();
71    },
72
73    stopCapturing: function()
74    {
75        TimelineAgent.stop();
76
77        // NOTE: Always stop immediately instead of waiting for a Timeline.recordingStopped event.
78        // This way the UI feels as responsive to a stop as possible.
79        this.capturingStopped();
80    },
81
82    capturingStarted: function()
83    {
84        if (this._isCapturing)
85            return;
86
87        this._isCapturing = true;
88
89        this.dispatchEventToListeners(WebInspector.TimelineManager.Event.CapturingStarted);
90    },
91
92    capturingStopped: function()
93    {
94        if (!this._isCapturing)
95            return;
96
97        if (this._stopCapturingTimeout) {
98            clearTimeout(this._stopCapturingTimeout);
99            delete this._stopCapturingTimeout;
100        }
101
102        if (this._deadTimeTimeout) {
103            clearTimeout(this._deadTimeTimeout);
104            delete this._deadTimeTimeout;
105        }
106
107        this._isCapturing = false;
108        this._autoCapturingMainResource = null;
109
110        this.dispatchEventToListeners(WebInspector.TimelineManager.Event.CapturingStopped);
111    },
112
113    eventRecorded: function(originalRecordPayload)
114    {
115        // Called from WebInspector.TimelineObserver.
116
117        if (!this._isCapturing)
118            return;
119
120        function processRecord(recordPayload, parentRecordPayload)
121        {
122            // Convert the timestamps to seconds to match the resource timestamps.
123            var startTime = recordPayload.startTime / 1000;
124            var endTime = recordPayload.endTime / 1000;
125
126            var callFrames = this._callFramesFromPayload(recordPayload.stackTrace);
127
128            var significantCallFrame = null;
129            if (callFrames) {
130                for (var i = 0; i < callFrames.length; ++i) {
131                    if (callFrames[i].nativeCode)
132                        continue;
133                    significantCallFrame = callFrames[i];
134                    break;
135                }
136            }
137
138            var sourceCodeLocation = significantCallFrame && significantCallFrame.sourceCodeLocation;
139
140            switch (recordPayload.type) {
141            case TimelineAgent.EventType.MarkLoad:
142                console.assert(isNaN(endTime));
143
144                var frame = WebInspector.frameResourceManager.frameForIdentifier(recordPayload.frameId);
145                console.assert(frame);
146                if (!frame)
147                    break;
148
149                frame.markLoadEvent(startTime);
150
151                if (!frame.isMainFrame())
152                    break;
153
154                var eventMarker = new WebInspector.TimelineMarker(startTime, WebInspector.TimelineMarker.Type.LoadEvent);
155                this._activeRecording.addEventMarker(eventMarker);
156
157                this._stopAutoRecordingSoon();
158                break;
159
160            case TimelineAgent.EventType.MarkDOMContent:
161                console.assert(isNaN(endTime));
162
163                var frame = WebInspector.frameResourceManager.frameForIdentifier(recordPayload.frameId);
164                console.assert(frame);
165                if (!frame)
166                    break;
167
168                frame.markDOMContentReadyEvent(startTime);
169
170                if (!frame.isMainFrame())
171                    break;
172
173                var eventMarker = new WebInspector.TimelineMarker(startTime, WebInspector.TimelineMarker.Type.DOMContentEvent);
174                this._activeRecording.addEventMarker(eventMarker);
175                break;
176
177            case TimelineAgent.EventType.ScheduleStyleRecalculation:
178                console.assert(isNaN(endTime));
179
180                // Pass the startTime as the endTime since this record type has no duration.
181                this._addRecord(new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.InvalidateStyles, startTime, startTime, callFrames, sourceCodeLocation));
182                break;
183
184            case TimelineAgent.EventType.RecalculateStyles:
185                this._addRecord(new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.RecalculateStyles, startTime, endTime, callFrames, sourceCodeLocation));
186                break;
187
188            case TimelineAgent.EventType.InvalidateLayout:
189                console.assert(isNaN(endTime));
190
191                // Pass the startTime as the endTime since this record type has no duration.
192                this._addRecord(new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.InvalidateLayout, startTime, startTime, callFrames, sourceCodeLocation));
193                break;
194
195            case TimelineAgent.EventType.Layout:
196                var layoutRecordType = sourceCodeLocation ? WebInspector.LayoutTimelineRecord.EventType.ForcedLayout : WebInspector.LayoutTimelineRecord.EventType.Layout;
197
198                // COMPATIBILITY (iOS 6): Layout records did not contain area properties. This is not exposed via a quad "root".
199                var quad = recordPayload.data.root ? new WebInspector.Quad(recordPayload.data.root) : null;
200                if (quad)
201                    this._addRecord(new WebInspector.LayoutTimelineRecord(layoutRecordType, startTime, endTime, callFrames, sourceCodeLocation, quad.points[0].x, quad.points[0].y, quad.width, quad.height, quad));
202                else
203                    this._addRecord(new WebInspector.LayoutTimelineRecord(layoutRecordType, startTime, endTime, callFrames, sourceCodeLocation));
204                break;
205
206            case TimelineAgent.EventType.Paint:
207                // COMPATIBILITY (iOS 6): Paint records data contained x, y, width, height properties. This became a quad "clip".
208                var quad = recordPayload.data.clip ? new WebInspector.Quad(recordPayload.data.clip) : null;
209                if (quad)
210                    this._addRecord(new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.Paint, startTime, endTime, callFrames, sourceCodeLocation, null, null, quad.width, quad.height, quad));
211                else
212                    this._addRecord(new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.Paint, startTime, endTime, callFrames, sourceCodeLocation, recordPayload.data.x, recordPayload.data.y, recordPayload.data.width, recordPayload.data.height));
213                break;
214
215            case TimelineAgent.EventType.EvaluateScript:
216                if (!sourceCodeLocation) {
217                    var mainFrame = WebInspector.frameResourceManager.mainFrame;
218                    var scriptResource = mainFrame.url === recordPayload.data.url ? mainFrame.mainResource : mainFrame.resourceForURL(recordPayload.data.url, true);
219                    if (scriptResource) {
220                        // The lineNumber is 1-based, but we expect 0-based.
221                        var lineNumber = recordPayload.data.lineNumber - 1;
222
223                        // FIXME: No column number is provided.
224                        sourceCodeLocation = scriptResource.createSourceCodeLocation(lineNumber, 0);
225                    }
226                }
227
228                var profileData = recordPayload.data.profile;
229
230                switch (parentRecordPayload && parentRecordPayload.type) {
231                case TimelineAgent.EventType.TimerFire:
232                    this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.TimerFired, startTime, endTime, callFrames, sourceCodeLocation, parentRecordPayload.data.timerId, profileData));
233                    break;
234                default:
235                    this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.ScriptEvaluated, startTime, endTime, callFrames, sourceCodeLocation, null, profileData));
236                    break;
237                }
238
239                break;
240
241            case TimelineAgent.EventType.TimeStamp:
242                var eventMarker = new WebInspector.TimelineMarker(startTime, WebInspector.TimelineMarker.Type.TimeStamp);
243                this._activeRecording.addEventMarker(eventMarker);
244                break;
245
246            case TimelineAgent.EventType.ConsoleProfile:
247                var profileData = recordPayload.data.profile;
248                console.assert(profileData);
249                this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.ConsoleProfileRecorded, startTime, endTime, callFrames, sourceCodeLocation, recordPayload.data.title, profileData));
250                break;
251
252            case TimelineAgent.EventType.FunctionCall:
253                // FunctionCall always happens as a child of another record, and since the FunctionCall record
254                // has useful info we just make the timeline record here (combining the data from both records).
255                if (!parentRecordPayload)
256                    break;
257
258                if (!sourceCodeLocation) {
259                    var mainFrame = WebInspector.frameResourceManager.mainFrame;
260                    var scriptResource = mainFrame.url === recordPayload.data.scriptName ? mainFrame.mainResource : mainFrame.resourceForURL(recordPayload.data.scriptName, true);
261                    if (scriptResource) {
262                        // The lineNumber is 1-based, but we expect 0-based.
263                        var lineNumber = recordPayload.data.scriptLine - 1;
264
265                        // FIXME: No column number is provided.
266                        sourceCodeLocation = scriptResource.createSourceCodeLocation(lineNumber, 0);
267                    }
268                }
269
270                var profileData = recordPayload.data.profile;
271
272                switch (parentRecordPayload.type) {
273                case TimelineAgent.EventType.TimerFire:
274                    this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.TimerFired, startTime, endTime, callFrames, sourceCodeLocation, parentRecordPayload.data.timerId, profileData));
275                    break;
276                case TimelineAgent.EventType.EventDispatch:
277                    this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.EventDispatched, startTime, endTime, callFrames, sourceCodeLocation, parentRecordPayload.data.type, profileData));
278                    break;
279                case TimelineAgent.EventType.XHRLoad:
280                    this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.EventDispatched, startTime, endTime, callFrames, sourceCodeLocation, "load", profileData));
281                    break;
282                case TimelineAgent.EventType.XHRReadyStateChange:
283                    this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.EventDispatched, startTime, endTime, callFrames, sourceCodeLocation, "readystatechange", profileData));
284                    break;
285                case TimelineAgent.EventType.FireAnimationFrame:
286                    this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.AnimationFrameFired, startTime, endTime, callFrames, sourceCodeLocation, parentRecordPayload.data.id, profileData));
287                    break;
288                }
289
290                break;
291
292            case TimelineAgent.EventType.ProbeSample:
293                // Pass the startTime as the endTime since this record type has no duration.
294                sourceCodeLocation = WebInspector.probeManager.probeForIdentifier(recordPayload.data.probeId).breakpoint.sourceCodeLocation;
295                this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.ProbeSampleRecorded, startTime, startTime, callFrames, sourceCodeLocation, recordPayload.data.probeId));
296                break;
297
298            case TimelineAgent.EventType.TimerInstall:
299                console.assert(isNaN(endTime));
300
301                // Pass the startTime as the endTime since this record type has no duration.
302                this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.TimerInstalled, startTime, startTime, callFrames, sourceCodeLocation, recordPayload.data.timerId));
303                break;
304
305            case TimelineAgent.EventType.TimerRemove:
306                console.assert(isNaN(endTime));
307
308                // Pass the startTime as the endTime since this record type has no duration.
309                this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.TimerRemoved, startTime, startTime, callFrames, sourceCodeLocation, recordPayload.data.timerId));
310                break;
311
312            case TimelineAgent.EventType.RequestAnimationFrame:
313                console.assert(isNaN(endTime));
314
315                // Pass the startTime as the endTime since this record type has no duration.
316                this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.AnimationFrameRequested, startTime, startTime, callFrames, sourceCodeLocation, recordPayload.data.timerId));
317                break;
318
319            case TimelineAgent.EventType.CancelAnimationFrame:
320                console.assert(isNaN(endTime));
321
322                // Pass the startTime as the endTime since this record type has no duration.
323                this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.AnimationFrameCanceled, startTime, startTime, callFrames, sourceCodeLocation, recordPayload.data.timerId));
324                break;
325            }
326        }
327
328        // Iterate over the records tree using a stack. Doing this recursively has
329        // been known to cause a call stack overflow. https://webkit.org/b/79106
330        var stack = [{array: [originalRecordPayload], parent: null, index: 0}];
331        while (stack.length) {
332            var entry = stack.lastValue;
333            var recordPayloads = entry.array;
334            var parentRecordPayload = entry.parent;
335
336            if (entry.index < recordPayloads.length) {
337                var recordPayload = recordPayloads[entry.index];
338
339                processRecord.call(this, recordPayload, parentRecordPayload);
340
341                if (recordPayload.children)
342                    stack.push({array: recordPayload.children, parent: recordPayload, index: 0});
343                ++entry.index;
344            } else
345                stack.pop();
346        }
347    },
348
349    pageDidLoad: function(timestamp)
350    {
351        if (isNaN(WebInspector.frameResourceManager.mainFrame.loadEventTimestamp))
352            WebInspector.frameResourceManager.mainFrame.markLoadEvent(timestamp);
353    },
354
355    // Private
356
357    _callFramesFromPayload: function(payload)
358    {
359        if (!payload)
360            return null;
361
362        function createCallFrame(payload)
363        {
364            var url = payload.url;
365            var nativeCode = false;
366
367            if (url === "[native code]") {
368                nativeCode = true;
369                url = null;
370            }
371
372            var sourceCode = WebInspector.frameResourceManager.resourceForURL(url);
373            if (!sourceCode)
374                sourceCode = WebInspector.debuggerManager.scriptsForURL(url)[0];
375
376            // The lineNumber is 1-based, but we expect 0-based.
377            var lineNumber = payload.lineNumber - 1;
378
379            var sourceCodeLocation = sourceCode ? sourceCode.createLazySourceCodeLocation(lineNumber, payload.columnNumber) : null;
380            var functionName = payload.functionName !== "global code" ? payload.functionName : null;
381
382            return new WebInspector.CallFrame(null, sourceCodeLocation, functionName, null, null, nativeCode);
383        }
384
385        return payload.map(createCallFrame);
386    },
387
388    _addRecord: function(record)
389    {
390        this._activeRecording.addRecord(record);
391
392        // Only worry about dead time after the load event.
393        if (!isNaN(WebInspector.frameResourceManager.mainFrame.loadEventTimestamp))
394            this._resetAutoRecordingDeadTimeTimeout();
395    },
396
397    _startAutoCapturing: function(event)
398    {
399        if (!event.target.isMainFrame() || (this._isCapturing && !this._autoCapturingMainResource))
400            return false;
401
402        var mainResource = event.target.provisionalMainResource || event.target.mainResource;
403        if (mainResource === this._autoCapturingMainResource)
404            return false;
405
406        this.stopCapturing();
407
408        this._autoCapturingMainResource = mainResource;
409
410        this._activeRecording.reset();
411
412        this.startCapturing();
413
414        this._addRecord(new WebInspector.ResourceTimelineRecord(mainResource));
415
416        if (this._stopCapturingTimeout)
417            clearTimeout(this._stopCapturingTimeout);
418        this._stopCapturingTimeout = setTimeout(this._boundStopCapturing, WebInspector.TimelineManager.MaximumAutoRecordDuration);
419
420        return true;
421    },
422
423    _stopAutoRecordingSoon: function()
424    {
425        // Only auto stop when auto capturing.
426        if (!this._isCapturing || !this._autoCapturingMainResource)
427            return;
428
429        if (this._stopCapturingTimeout)
430            clearTimeout(this._stopCapturingTimeout);
431        this._stopCapturingTimeout = setTimeout(this._boundStopCapturing, WebInspector.TimelineManager.MaximumAutoRecordDurationAfterLoadEvent);
432    },
433
434    _resetAutoRecordingDeadTimeTimeout: function()
435    {
436        // Only monitor dead time when auto capturing.
437        if (!this._isCapturing || !this._autoCapturingMainResource)
438            return;
439
440        if (this._deadTimeTimeout)
441            clearTimeout(this._deadTimeTimeout);
442        this._deadTimeTimeout = setTimeout(this._boundStopCapturing, WebInspector.TimelineManager.DeadTimeRequiredToStopAutoRecordingEarly);
443    },
444
445    _mainResourceDidChange: function(event)
446    {
447        // Ignore resource events when there isn't a main frame yet. Those events are triggered by
448        // loading the cached resources when the inspector opens, and they do not have timing information.
449        if (!WebInspector.frameResourceManager.mainFrame)
450            return;
451
452        if (this._startAutoCapturing(event))
453            return;
454
455        if (!this._isCapturing)
456            return;
457
458        var mainResource = event.target.mainResource;
459        if (mainResource === this._autoCapturingMainResource)
460            return;
461
462        this._addRecord(new WebInspector.ResourceTimelineRecord(mainResource));
463    },
464
465    _resourceWasAdded: function(event)
466    {
467        // Ignore resource events when there isn't a main frame yet. Those events are triggered by
468        // loading the cached resources when the inspector opens, and they do not have timing information.
469        if (!WebInspector.frameResourceManager.mainFrame)
470            return;
471
472        if (!this._isCapturing)
473            return;
474
475        this._addRecord(new WebInspector.ResourceTimelineRecord(event.data.resource));
476    }
477};
478
479WebInspector.TimelineManager.prototype.__proto__ = WebInspector.Object.prototype;
480