Web Inspector: discontinuous recordings should have discontinuities in the timeline...
[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         this._discontinuities = [];
163
164         this._topDownCallingContextTree.reset();
165         this._bottomUpCallingContextTree.reset();
166
167         for (var timeline of this._timelines.values())
168             timeline.reset(suppressEvents);
169
170         WebInspector.RenderingFrameTimelineRecord.resetFrameIndex();
171
172         if (!suppressEvents) {
173             this.dispatchEventToListeners(WebInspector.TimelineRecording.Event.Reset);
174             this.dispatchEventToListeners(WebInspector.TimelineRecording.Event.TimesUpdated);
175         }
176     }
177
178     sourceCodeTimelinesForSourceCode(sourceCode)
179     {
180         var timelines = this._sourceCodeTimelinesMap.get(sourceCode);
181         if (!timelines)
182             return [];
183         return [...timelines.values()];
184     }
185
186     timelineForInstrument(instrument)
187     {
188         return this._timelines.get(instrument.timelineRecordType);
189     }
190
191     instrumentForTimeline(timeline)
192     {
193         return this._instruments.find((instrument) => instrument.timelineRecordType === timeline.type);
194     }
195
196     timelineForRecordType(recordType)
197     {
198         return this._timelines.get(recordType);
199     }
200
201     addInstrument(instrument)
202     {
203         console.assert(instrument instanceof WebInspector.Instrument, instrument);
204         console.assert(!this._instruments.includes(instrument), this._instruments, instrument);
205
206         this._instruments.push(instrument);
207
208         this.dispatchEventToListeners(WebInspector.TimelineRecording.Event.InstrumentAdded, {instrument});
209     }
210
211     removeInstrument(instrument)
212     {
213         console.assert(instrument instanceof WebInspector.Instrument, instrument);
214         console.assert(this._instruments.includes(instrument), this._instruments, instrument);
215
216         this._instruments.remove(instrument);
217
218         this.dispatchEventToListeners(WebInspector.TimelineRecording.Event.InstrumentRemoved, {instrument});
219     }
220
221     addEventMarker(marker)
222     {
223         if (!this._capturing)
224             return;
225
226         this._eventMarkers.push(marker);
227
228         this.dispatchEventToListeners(WebInspector.TimelineRecording.Event.MarkerAdded, {marker});
229     }
230
231     addRecord(record)
232     {
233         var timeline = this._timelines.get(record.type);
234         console.assert(timeline, record, this._timelines);
235         if (!timeline)
236             return;
237
238         // Add the record to the global timeline by type.
239         timeline.addRecord(record);
240
241         // Some records don't have source code timelines.
242         if (record.type === WebInspector.TimelineRecord.Type.Network
243             || record.type === WebInspector.TimelineRecord.Type.RenderingFrame
244             || record.type === WebInspector.TimelineRecord.Type.Memory
245             || record.type === WebInspector.TimelineRecord.Type.HeapAllocations)
246             return;
247
248         if (!WebInspector.TimelineRecording.sourceCodeTimelinesSupported())
249             return;
250
251         // Add the record to the source code timelines.
252         var activeMainResource = WebInspector.frameResourceManager.mainFrame.provisionalMainResource || WebInspector.frameResourceManager.mainFrame.mainResource;
253         var sourceCode = record.sourceCodeLocation ? record.sourceCodeLocation.sourceCode : activeMainResource;
254
255         var sourceCodeTimelines = this._sourceCodeTimelinesMap.get(sourceCode);
256         if (!sourceCodeTimelines) {
257             sourceCodeTimelines = new Map;
258             this._sourceCodeTimelinesMap.set(sourceCode, sourceCodeTimelines);
259         }
260
261         var newTimeline = false;
262         var key = this._keyForRecord(record);
263         var sourceCodeTimeline = sourceCodeTimelines.get(key);
264         if (!sourceCodeTimeline) {
265             sourceCodeTimeline = new WebInspector.SourceCodeTimeline(sourceCode, record.sourceCodeLocation, record.type, record.eventType);
266             sourceCodeTimelines.set(key, sourceCodeTimeline);
267             newTimeline = true;
268         }
269
270         sourceCodeTimeline.addRecord(record);
271
272         if (newTimeline)
273             this.dispatchEventToListeners(WebInspector.TimelineRecording.Event.SourceCodeTimelineAdded, {sourceCodeTimeline});
274     }
275
276     addMemoryPressureEvent(memoryPressureEvent)
277     {
278         let memoryTimeline = this._timelines.get(WebInspector.TimelineRecord.Type.Memory);
279         console.assert(memoryTimeline, this._timelines);
280         if (!memoryTimeline)
281             return;
282
283         memoryTimeline.addMemoryPressureEvent(memoryPressureEvent);
284     }
285
286     addDiscontinuity(startTime, endTime)
287     {
288         this._discontinuities.push({startTime, endTime});
289     }
290
291     discontinuitiesInTimeRange(startTime, endTime)
292     {
293         return this._discontinuities.filter((item) => item.startTime < endTime && item.endTime > startTime);
294     }
295
296     computeElapsedTime(timestamp)
297     {
298         if (!timestamp || isNaN(timestamp))
299             return NaN;
300
301         // COMPATIBILITY (iOS 8): old backends send timestamps (seconds or milliseconds since the epoch),
302         // rather than seconds elapsed since timeline capturing started. We approximate the latter by
303         // subtracting the start timestamp, as old versions did not use monotonic times.
304         if (WebInspector.TimelineRecording.isLegacy === undefined)
305             WebInspector.TimelineRecording.isLegacy = timestamp > WebInspector.TimelineRecording.TimestampThresholdForLegacyRecordConversion;
306
307         if (!WebInspector.TimelineRecording.isLegacy)
308             return timestamp;
309
310         // If the record's start time is large, but not really large, then it is seconds since epoch
311         // not millseconds since epoch, so convert it to milliseconds.
312         if (timestamp < WebInspector.TimelineRecording.TimestampThresholdForLegacyAssumedMilliseconds)
313             timestamp *= 1000;
314
315         if (isNaN(this._legacyFirstRecordedTimestamp))
316             this._legacyFirstRecordedTimestamp = timestamp;
317
318         // Return seconds since the first recorded value.
319         return (timestamp - this._legacyFirstRecordedTimestamp) / 1000.0;
320     }
321
322     setLegacyBaseTimestamp(timestamp)
323     {
324         console.assert(isNaN(this._legacyFirstRecordedTimestamp));
325
326         if (timestamp < WebInspector.TimelineRecording.TimestampThresholdForLegacyAssumedMilliseconds)
327             timestamp *= 1000;
328
329         this._legacyFirstRecordedTimestamp = timestamp;
330     }
331
332     initializeTimeBoundsIfNecessary(timestamp)
333     {
334         if (isNaN(this._startTime)) {
335             console.assert(isNaN(this._endTime));
336
337             this._startTime = timestamp;
338             this._endTime = timestamp;
339
340             this.dispatchEventToListeners(WebInspector.TimelineRecording.Event.TimesUpdated);
341         }
342     }
343
344     // Private
345
346     _keyForRecord(record)
347     {
348         var key = record.type;
349         if (record instanceof WebInspector.ScriptTimelineRecord || record instanceof WebInspector.LayoutTimelineRecord)
350             key += ":" + record.eventType;
351         if (record instanceof WebInspector.ScriptTimelineRecord && record.eventType === WebInspector.ScriptTimelineRecord.EventType.EventDispatched)
352             key += ":" + record.details;
353         if (record.sourceCodeLocation)
354             key += ":" + record.sourceCodeLocation.lineNumber + ":" + record.sourceCodeLocation.columnNumber;
355         return key;
356     }
357
358     _timelineTimesUpdated(event)
359     {
360         var timeline = event.target;
361         var changed = false;
362
363         if (isNaN(this._startTime) || timeline.startTime < this._startTime) {
364             this._startTime = timeline.startTime;
365             changed = true;
366         }
367
368         if (isNaN(this._endTime) || this._endTime < timeline.endTime) {
369             this._endTime = timeline.endTime;
370             changed = true;
371         }
372
373         if (changed)
374             this.dispatchEventToListeners(WebInspector.TimelineRecording.Event.TimesUpdated);
375     }
376 };
377
378 WebInspector.TimelineRecording.Event = {
379     Reset: "timeline-recording-reset",
380     Unloaded: "timeline-recording-unloaded",
381     SourceCodeTimelineAdded: "timeline-recording-source-code-timeline-added",
382     InstrumentAdded: "timeline-recording-instrument-added",
383     InstrumentRemoved: "timeline-recording-instrument-removed",
384     TimesUpdated: "timeline-recording-times-updated",
385     MarkerAdded: "timeline-recording-marker-added",
386 };
387
388 WebInspector.TimelineRecording.isLegacy = undefined;
389 WebInspector.TimelineRecording.TimestampThresholdForLegacyRecordConversion = 10000000; // Some value not near zero.
390 WebInspector.TimelineRecording.TimestampThresholdForLegacyAssumedMilliseconds = 1420099200000; // Date.parse("Jan 1, 2015"). Milliseconds since epoch.