932568d950c98919ed7c7a91a3bd2f330a963b92
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Models / TimelineRecording.js
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
26 WebInspector.TimelineRecording = class TimelineRecording extends WebInspector.Object
27 {
28     constructor(identifier, displayName, instruments)
29     {
30         super();
31
32         this._identifier = identifier;
33         this._timelines = new Map;
34         this._displayName = displayName;
35         this._capturing = false;
36         this._readonly = false;
37         this._instruments = instruments || [];
38         this._topDownCallingContextTree = new WebInspector.CallingContextTree(WebInspector.CallingContextTree.Type.TopDown);
39         this._bottomUpCallingContextTree = new WebInspector.CallingContextTree(WebInspector.CallingContextTree.Type.BottomUp);
40
41         for (let type of WebInspector.TimelineManager.availableTimelineTypes()) {
42             let timeline = WebInspector.Timeline.create(type);
43             this._timelines.set(type, timeline);
44             timeline.addEventListener(WebInspector.Timeline.Event.TimesUpdated, this._timelineTimesUpdated, this);
45         }
46
47         // For legacy backends, we compute the elapsed time of records relative to this timestamp.
48         this._legacyFirstRecordedTimestamp = NaN;
49
50         this.reset(true);
51     }
52
53     // Static
54
55     static sourceCodeTimelinesSupported()
56     {
57         return WebInspector.debuggableType === WebInspector.DebuggableType.Web;
58     }
59
60     // Public
61
62     get displayName()
63     {
64         return this._displayName;
65     }
66
67     get identifier()
68     {
69         return this._identifier;
70     }
71
72     get timelines()
73     {
74         return this._timelines;
75     }
76
77     get instruments()
78     {
79         return this._instruments;
80     }
81
82     get readonly()
83     {
84         return this._readonly;
85     }
86
87     get startTime()
88     {
89         return this._startTime;
90     }
91
92     get endTime()
93     {
94         return this._endTime;
95     }
96
97     get topDownCallingContextTree()
98     {
99         return this._topDownCallingContextTree;
100     }
101
102     get bottomUpCallingContextTree()
103     {
104         return this._bottomUpCallingContextTree;
105     }
106
107     start()
108     {
109         console.assert(!this._capturing, "Attempted to start an already started session.");
110         console.assert(!this._readonly, "Attempted to start a readonly session.");
111
112         this._capturing = true;
113
114         for (let instrument of this._instruments)
115             instrument.startInstrumentation();
116     }
117
118     stop()
119     {
120         console.assert(this._capturing, "Attempted to stop an already stopped session.");
121         console.assert(!this._readonly, "Attempted to stop a readonly session.");
122
123         this._capturing = false;
124
125         for (let instrument of this._instruments)
126             instrument.stopInstrumentation();
127     }
128
129     saveIdentityToCookie()
130     {
131         // Do nothing. Timeline recordings are not persisted when the inspector is
132         // re-opened, so do not attempt to restore by identifier or display name.
133     }
134
135     isEmpty()
136     {
137         for (var timeline of this._timelines.values()) {
138             if (timeline.records.length)
139                 return false;
140         }
141
142         return true;
143     }
144
145     unloaded()
146     {
147         console.assert(!this.isEmpty(), "Shouldn't unload an empty recording; it should be reused instead.");
148
149         this._readonly = true;
150
151         this.dispatchEventToListeners(WebInspector.TimelineRecording.Event.Unloaded);
152     }
153
154     reset(suppressEvents)
155     {
156         console.assert(!this._readonly, "Can't reset a read-only recording.");
157
158         this._sourceCodeTimelinesMap = new Map;
159         this._eventMarkers = [];
160         this._startTime = NaN;
161         this._endTime = NaN;
162
163         this._topDownCallingContextTree.reset();
164         this._bottomUpCallingContextTree.reset();
165
166         for (var timeline of this._timelines.values())
167             timeline.reset(suppressEvents);
168
169         WebInspector.RenderingFrameTimelineRecord.resetFrameIndex();
170
171         if (!suppressEvents) {
172             this.dispatchEventToListeners(WebInspector.TimelineRecording.Event.Reset);
173             this.dispatchEventToListeners(WebInspector.TimelineRecording.Event.TimesUpdated);
174         }
175     }
176
177     sourceCodeTimelinesForSourceCode(sourceCode)
178     {
179         var timelines = this._sourceCodeTimelinesMap.get(sourceCode);
180         if (!timelines)
181             return [];
182         return [...timelines.values()];
183     }
184
185     timelineForInstrument(instrument)
186     {
187         return this._timelines.get(instrument.timelineRecordType);
188     }
189
190     instrumentForTimeline(timeline)
191     {
192         return this._instruments.find((instrument) => instrument.timelineRecordType === timeline.type);
193     }
194
195     timelineForRecordType(recordType)
196     {
197         return this._timelines.get(recordType);
198     }
199
200     addInstrument(instrument)
201     {
202         console.assert(instrument instanceof WebInspector.Instrument, instrument);
203         console.assert(!this._instruments.includes(instrument), this._instruments, instrument);
204
205         this._instruments.push(instrument);
206
207         this.dispatchEventToListeners(WebInspector.TimelineRecording.Event.InstrumentAdded, {instrument});
208     }
209
210     removeInstrument(instrument)
211     {
212         console.assert(instrument instanceof WebInspector.Instrument, instrument);
213         console.assert(this._instruments.includes(instrument), this._instruments, instrument);
214
215         this._instruments.remove(instrument);
216
217         this.dispatchEventToListeners(WebInspector.TimelineRecording.Event.InstrumentRemoved, {instrument});
218     }
219
220     addEventMarker(marker)
221     {
222         if (!this._capturing)
223             return;
224
225         this._eventMarkers.push(marker);
226
227         this.dispatchEventToListeners(WebInspector.TimelineRecording.Event.MarkerAdded, {marker});
228     }
229
230     addRecord(record)
231     {
232         var timeline = this._timelines.get(record.type);
233         console.assert(timeline, record, this._timelines);
234         if (!timeline)
235             return;
236
237         // Add the record to the global timeline by type.
238         timeline.addRecord(record);
239
240         // Some records don't have source code timelines.
241         if (record.type === WebInspector.TimelineRecord.Type.Network
242             || record.type === WebInspector.TimelineRecord.Type.RenderingFrame
243             || record.type === WebInspector.TimelineRecord.Type.Memory
244             || record.type === WebInspector.TimelineRecord.Type.HeapAllocations)
245             return;
246
247         if (!WebInspector.TimelineRecording.sourceCodeTimelinesSupported())
248             return;
249
250         // Add the record to the source code timelines.
251         var activeMainResource = WebInspector.frameResourceManager.mainFrame.provisionalMainResource || WebInspector.frameResourceManager.mainFrame.mainResource;
252         var sourceCode = record.sourceCodeLocation ? record.sourceCodeLocation.sourceCode : activeMainResource;
253
254         var sourceCodeTimelines = this._sourceCodeTimelinesMap.get(sourceCode);
255         if (!sourceCodeTimelines) {
256             sourceCodeTimelines = new Map;
257             this._sourceCodeTimelinesMap.set(sourceCode, sourceCodeTimelines);
258         }
259
260         var newTimeline = false;
261         var key = this._keyForRecord(record);
262         var sourceCodeTimeline = sourceCodeTimelines.get(key);
263         if (!sourceCodeTimeline) {
264             sourceCodeTimeline = new WebInspector.SourceCodeTimeline(sourceCode, record.sourceCodeLocation, record.type, record.eventType);
265             sourceCodeTimelines.set(key, sourceCodeTimeline);
266             newTimeline = true;
267         }
268
269         sourceCodeTimeline.addRecord(record);
270
271         if (newTimeline)
272             this.dispatchEventToListeners(WebInspector.TimelineRecording.Event.SourceCodeTimelineAdded, {sourceCodeTimeline});
273     }
274
275     addMemoryPressureEvent(memoryPressureEvent)
276     {
277         let memoryTimeline = this._timelines.get(WebInspector.TimelineRecord.Type.Memory);
278         console.assert(memoryTimeline, this._timelines);
279         if (!memoryTimeline)
280             return;
281
282         memoryTimeline.addMemoryPressureEvent(memoryPressureEvent);
283     }
284
285     computeElapsedTime(timestamp)
286     {
287         if (!timestamp || isNaN(timestamp))
288             return NaN;
289
290         // COMPATIBILITY (iOS 8): old backends send timestamps (seconds or milliseconds since the epoch),
291         // rather than seconds elapsed since timeline capturing started. We approximate the latter by
292         // subtracting the start timestamp, as old versions did not use monotonic times.
293         if (WebInspector.TimelineRecording.isLegacy === undefined)
294             WebInspector.TimelineRecording.isLegacy = timestamp > WebInspector.TimelineRecording.TimestampThresholdForLegacyRecordConversion;
295
296         if (!WebInspector.TimelineRecording.isLegacy)
297             return timestamp;
298
299         // If the record's start time is large, but not really large, then it is seconds since epoch
300         // not millseconds since epoch, so convert it to milliseconds.
301         if (timestamp < WebInspector.TimelineRecording.TimestampThresholdForLegacyAssumedMilliseconds)
302             timestamp *= 1000;
303
304         if (isNaN(this._legacyFirstRecordedTimestamp))
305             this._legacyFirstRecordedTimestamp = timestamp;
306
307         // Return seconds since the first recorded value.
308         return (timestamp - this._legacyFirstRecordedTimestamp) / 1000.0;
309     }
310
311     setLegacyBaseTimestamp(timestamp)
312     {
313         console.assert(isNaN(this._legacyFirstRecordedTimestamp));
314
315         if (timestamp < WebInspector.TimelineRecording.TimestampThresholdForLegacyAssumedMilliseconds)
316             timestamp *= 1000;
317
318         this._legacyFirstRecordedTimestamp = timestamp;
319     }
320
321     initializeTimeBoundsIfNecessary(timestamp)
322     {
323         if (isNaN(this._startTime)) {
324             console.assert(isNaN(this._endTime));
325
326             this._startTime = timestamp;
327             this._endTime = timestamp;
328
329             this.dispatchEventToListeners(WebInspector.TimelineRecording.Event.TimesUpdated);
330         }
331     }
332
333     // Private
334
335     _keyForRecord(record)
336     {
337         var key = record.type;
338         if (record instanceof WebInspector.ScriptTimelineRecord || record instanceof WebInspector.LayoutTimelineRecord)
339             key += ":" + record.eventType;
340         if (record instanceof WebInspector.ScriptTimelineRecord && record.eventType === WebInspector.ScriptTimelineRecord.EventType.EventDispatched)
341             key += ":" + record.details;
342         if (record.sourceCodeLocation)
343             key += ":" + record.sourceCodeLocation.lineNumber + ":" + record.sourceCodeLocation.columnNumber;
344         return key;
345     }
346
347     _timelineTimesUpdated(event)
348     {
349         var timeline = event.target;
350         var changed = false;
351
352         if (isNaN(this._startTime) || timeline.startTime < this._startTime) {
353             this._startTime = timeline.startTime;
354             changed = true;
355         }
356
357         if (isNaN(this._endTime) || this._endTime < timeline.endTime) {
358             this._endTime = timeline.endTime;
359             changed = true;
360         }
361
362         if (changed)
363             this.dispatchEventToListeners(WebInspector.TimelineRecording.Event.TimesUpdated);
364     }
365 };
366
367 WebInspector.TimelineRecording.Event = {
368     Reset: "timeline-recording-reset",
369     Unloaded: "timeline-recording-unloaded",
370     SourceCodeTimelineAdded: "timeline-recording-source-code-timeline-added",
371     InstrumentAdded: "timeline-recording-instrument-added",
372     InstrumentRemoved: "timeline-recording-instrument-removed",
373     TimesUpdated: "timeline-recording-times-updated",
374     MarkerAdded: "timeline-recording-marker-added",
375 };
376
377 WebInspector.TimelineRecording.isLegacy = undefined;
378 WebInspector.TimelineRecording.TimestampThresholdForLegacyRecordConversion = 10000000; // Some value not near zero.
379 WebInspector.TimelineRecording.TimestampThresholdForLegacyAssumedMilliseconds = 1420099200000; // Date.parse("Jan 1, 2015"). Milliseconds since epoch.