summaryrefslogtreecommitdiff
path: root/Source/WebInspectorUI/UserInterface/Controllers/TimelineManager.js
diff options
context:
space:
mode:
authorLorry Tar Creator <lorry-tar-importer@lorry>2017-06-27 06:07:23 +0000
committerLorry Tar Creator <lorry-tar-importer@lorry>2017-06-27 06:07:23 +0000
commit1bf1084f2b10c3b47fd1a588d85d21ed0eb41d0c (patch)
tree46dcd36c86e7fbc6e5df36deb463b33e9967a6f7 /Source/WebInspectorUI/UserInterface/Controllers/TimelineManager.js
parent32761a6cee1d0dee366b885b7b9c777e67885688 (diff)
downloadWebKitGtk-tarball-master.tar.gz
Diffstat (limited to 'Source/WebInspectorUI/UserInterface/Controllers/TimelineManager.js')
-rw-r--r--Source/WebInspectorUI/UserInterface/Controllers/TimelineManager.js1091
1 files changed, 1091 insertions, 0 deletions
diff --git a/Source/WebInspectorUI/UserInterface/Controllers/TimelineManager.js b/Source/WebInspectorUI/UserInterface/Controllers/TimelineManager.js
new file mode 100644
index 000000000..e03ba9ba2
--- /dev/null
+++ b/Source/WebInspectorUI/UserInterface/Controllers/TimelineManager.js
@@ -0,0 +1,1091 @@
+/*
+ * Copyright (C) 2013, 2016 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+WebInspector.TimelineManager = class TimelineManager extends WebInspector.Object
+{
+ constructor()
+ {
+ super();
+
+ WebInspector.Frame.addEventListener(WebInspector.Frame.Event.ProvisionalLoadStarted, this._provisionalLoadStarted, this);
+ WebInspector.Frame.addEventListener(WebInspector.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
+ WebInspector.Frame.addEventListener(WebInspector.Frame.Event.ResourceWasAdded, this._resourceWasAdded, this);
+ WebInspector.Target.addEventListener(WebInspector.Target.Event.ResourceAdded, this._resourceWasAdded, this);
+
+ WebInspector.heapManager.addEventListener(WebInspector.HeapManager.Event.GarbageCollected, this._garbageCollected, this);
+ WebInspector.memoryManager.addEventListener(WebInspector.MemoryManager.Event.MemoryPressure, this._memoryPressure, this);
+
+ this._enabledTimelineTypesSetting = new WebInspector.Setting("enabled-instrument-types", WebInspector.TimelineManager.defaultTimelineTypes());
+ this._updateAutoCaptureInstruments();
+
+ this._persistentNetworkTimeline = new WebInspector.NetworkTimeline;
+
+ this._isCapturing = false;
+ this._initiatedByBackendStart = false;
+ this._initiatedByBackendStop = false;
+ this._waitingForCapturingStartedEvent = false;
+ this._isCapturingPageReload = false;
+ this._autoCaptureOnPageLoad = false;
+ this._mainResourceForAutoCapturing = null;
+ this._shouldSetAutoCapturingMainResource = false;
+ this._boundStopCapturing = this.stopCapturing.bind(this);
+
+ this._webTimelineScriptRecordsExpectingScriptProfilerEvents = null;
+ this._scriptProfilerRecords = null;
+
+ this._stopCapturingTimeout = undefined;
+ this._deadTimeTimeout = undefined;
+ this._lastDeadTimeTickle = 0;
+
+ this.reset();
+ }
+
+ // Static
+
+ static defaultTimelineTypes()
+ {
+ if (WebInspector.debuggableType === WebInspector.DebuggableType.JavaScript) {
+ let defaultTypes = [WebInspector.TimelineRecord.Type.Script];
+ if (WebInspector.HeapAllocationsInstrument.supported())
+ defaultTypes.push(WebInspector.TimelineRecord.Type.HeapAllocations);
+ return defaultTypes;
+ }
+
+ let defaultTypes = [
+ WebInspector.TimelineRecord.Type.Network,
+ WebInspector.TimelineRecord.Type.Layout,
+ WebInspector.TimelineRecord.Type.Script,
+ ];
+
+ if (WebInspector.FPSInstrument.supported())
+ defaultTypes.push(WebInspector.TimelineRecord.Type.RenderingFrame);
+
+ return defaultTypes;
+ }
+
+ static availableTimelineTypes()
+ {
+ let types = WebInspector.TimelineManager.defaultTimelineTypes();
+ if (WebInspector.debuggableType === WebInspector.DebuggableType.JavaScript)
+ return types;
+
+ if (WebInspector.MemoryInstrument.supported())
+ types.push(WebInspector.TimelineRecord.Type.Memory);
+
+ if (WebInspector.HeapAllocationsInstrument.supported())
+ types.push(WebInspector.TimelineRecord.Type.HeapAllocations);
+
+ return types;
+ }
+
+ // Public
+
+ reset()
+ {
+ if (this._isCapturing)
+ this.stopCapturing();
+
+ this._recordings = [];
+ this._activeRecording = null;
+ this._nextRecordingIdentifier = 1;
+
+ this._loadNewRecording();
+ }
+
+ // The current recording that new timeline records will be appended to, if any.
+ get activeRecording()
+ {
+ console.assert(this._activeRecording || !this._isCapturing);
+ return this._activeRecording;
+ }
+
+ get persistentNetworkTimeline()
+ {
+ return this._persistentNetworkTimeline;
+ }
+
+ get recordings()
+ {
+ return this._recordings.slice();
+ }
+
+ get autoCaptureOnPageLoad()
+ {
+ return this._autoCaptureOnPageLoad;
+ }
+
+ set autoCaptureOnPageLoad(autoCapture)
+ {
+ autoCapture = !!autoCapture;
+
+ if (this._autoCaptureOnPageLoad === autoCapture)
+ return;
+
+ this._autoCaptureOnPageLoad = autoCapture;
+
+ if (window.TimelineAgent && TimelineAgent.setAutoCaptureEnabled)
+ TimelineAgent.setAutoCaptureEnabled(this._autoCaptureOnPageLoad);
+ }
+
+ get enabledTimelineTypes()
+ {
+ let availableTimelineTypes = WebInspector.TimelineManager.availableTimelineTypes();
+ return this._enabledTimelineTypesSetting.value.filter((type) => availableTimelineTypes.includes(type));
+ }
+
+ set enabledTimelineTypes(x)
+ {
+ this._enabledTimelineTypesSetting.value = x || [];
+
+ this._updateAutoCaptureInstruments();
+ }
+
+ isCapturing()
+ {
+ return this._isCapturing;
+ }
+
+ isCapturingPageReload()
+ {
+ return this._isCapturingPageReload;
+ }
+
+ startCapturing(shouldCreateRecording)
+ {
+ console.assert(!this._isCapturing, "TimelineManager is already capturing.");
+
+ if (!this._activeRecording || shouldCreateRecording)
+ this._loadNewRecording();
+
+ this._waitingForCapturingStartedEvent = true;
+
+ this.dispatchEventToListeners(WebInspector.TimelineManager.Event.CapturingWillStart);
+
+ this._activeRecording.start(this._initiatedByBackendStart);
+ }
+
+ stopCapturing()
+ {
+ console.assert(this._isCapturing, "TimelineManager is not capturing.");
+
+ this._activeRecording.stop(this._initiatedByBackendStop);
+
+ // NOTE: Always stop immediately instead of waiting for a Timeline.recordingStopped event.
+ // This way the UI feels as responsive to a stop as possible.
+ // FIXME: <https://webkit.org/b/152904> Web Inspector: Timeline UI should keep up with processing all incoming records
+ this.capturingStopped();
+ }
+
+ unloadRecording()
+ {
+ if (!this._activeRecording)
+ return;
+
+ if (this._isCapturing)
+ this.stopCapturing();
+
+ this._activeRecording.unloaded();
+ this._activeRecording = null;
+ }
+
+ computeElapsedTime(timestamp)
+ {
+ if (!this._activeRecording)
+ return 0;
+
+ return this._activeRecording.computeElapsedTime(timestamp);
+ }
+
+ scriptProfilerIsTracking()
+ {
+ return this._scriptProfilerRecords !== null;
+ }
+
+ // Protected
+
+ capturingStarted(startTime)
+ {
+ // Called from WebInspector.TimelineObserver.
+
+ if (this._isCapturing)
+ return;
+
+ this._waitingForCapturingStartedEvent = false;
+ this._isCapturing = true;
+
+ this._lastDeadTimeTickle = 0;
+
+ if (startTime)
+ this.activeRecording.initializeTimeBoundsIfNecessary(startTime);
+
+ this._webTimelineScriptRecordsExpectingScriptProfilerEvents = [];
+
+ this.dispatchEventToListeners(WebInspector.TimelineManager.Event.CapturingStarted, {startTime});
+ }
+
+ capturingStopped(endTime)
+ {
+ // Called from WebInspector.TimelineObserver.
+
+ if (!this._isCapturing)
+ return;
+
+ if (this._stopCapturingTimeout) {
+ clearTimeout(this._stopCapturingTimeout);
+ this._stopCapturingTimeout = undefined;
+ }
+
+ if (this._deadTimeTimeout) {
+ clearTimeout(this._deadTimeTimeout);
+ this._deadTimeTimeout = undefined;
+ }
+
+ this._isCapturing = false;
+ this._isCapturingPageReload = false;
+ this._shouldSetAutoCapturingMainResource = false;
+ this._mainResourceForAutoCapturing = null;
+ this._initiatedByBackendStart = false;
+ this._initiatedByBackendStop = false;
+
+ this.dispatchEventToListeners(WebInspector.TimelineManager.Event.CapturingStopped, {endTime});
+ }
+
+ autoCaptureStarted()
+ {
+ // Called from WebInspector.TimelineObserver.
+
+ if (this._isCapturing)
+ this.stopCapturing();
+
+ this._initiatedByBackendStart = true;
+
+ // We may already have an fresh TimelineRecording created if autoCaptureStarted is received
+ // between sending the Timeline.start command and receiving Timeline.capturingStarted event.
+ // In that case, there is no need to call startCapturing again. Reuse the fresh recording.
+ if (!this._waitingForCapturingStartedEvent) {
+ const createNewRecording = true;
+ this.startCapturing(createNewRecording);
+ }
+
+ this._shouldSetAutoCapturingMainResource = true;
+ }
+
+ programmaticCaptureStarted()
+ {
+ // Called from WebInspector.TimelineObserver.
+
+ this._initiatedByBackendStart = true;
+
+ this._activeRecording.addScriptInstrumentForProgrammaticCapture();
+
+ const createNewRecording = false;
+ this.startCapturing(createNewRecording);
+ }
+
+ programmaticCaptureStopped()
+ {
+ // Called from WebInspector.TimelineObserver.
+
+ this._initiatedByBackendStop = true;
+
+ // FIXME: This is purely to avoid a noisy assert. Previously
+ // it was impossible to stop without stopping from the UI.
+ console.assert(!this._isCapturing);
+ this._isCapturing = true;
+
+ this.stopCapturing();
+ }
+
+ eventRecorded(recordPayload)
+ {
+ // Called from WebInspector.TimelineObserver.
+
+ if (!this._isCapturing)
+ return;
+
+ var records = [];
+
+ // Iterate over the records tree using a stack. Doing this recursively has
+ // been known to cause a call stack overflow. https://webkit.org/b/79106
+ var stack = [{array: [recordPayload], parent: null, parentRecord: null, index: 0}];
+ while (stack.length) {
+ var entry = stack.lastValue;
+ var recordPayloads = entry.array;
+
+ if (entry.index < recordPayloads.length) {
+ var recordPayload = recordPayloads[entry.index];
+ var record = this._processEvent(recordPayload, entry.parent);
+ if (record) {
+ record.parent = entry.parentRecord;
+ records.push(record);
+ if (entry.parentRecord)
+ entry.parentRecord.children.push(record);
+ }
+
+ if (recordPayload.children && recordPayload.children.length)
+ stack.push({array: recordPayload.children, parent: recordPayload, parentRecord: record || entry.parentRecord, index: 0});
+ ++entry.index;
+ } else
+ stack.pop();
+ }
+
+ for (var record of records) {
+ if (record.type === WebInspector.TimelineRecord.Type.RenderingFrame) {
+ if (!record.children.length)
+ continue;
+ record.setupFrameIndex();
+ }
+
+ this._addRecord(record);
+ }
+ }
+
+ // Protected
+
+ pageDOMContentLoadedEventFired(timestamp)
+ {
+ // Called from WebInspector.PageObserver.
+
+ console.assert(this._activeRecording);
+ console.assert(isNaN(WebInspector.frameResourceManager.mainFrame.domContentReadyEventTimestamp));
+
+ let computedTimestamp = this.activeRecording.computeElapsedTime(timestamp);
+
+ WebInspector.frameResourceManager.mainFrame.markDOMContentReadyEvent(computedTimestamp);
+
+ let eventMarker = new WebInspector.TimelineMarker(computedTimestamp, WebInspector.TimelineMarker.Type.DOMContentEvent);
+ this._activeRecording.addEventMarker(eventMarker);
+ }
+
+ pageLoadEventFired(timestamp)
+ {
+ // Called from WebInspector.PageObserver.
+
+ console.assert(this._activeRecording);
+ console.assert(isNaN(WebInspector.frameResourceManager.mainFrame.loadEventTimestamp));
+
+ let computedTimestamp = this.activeRecording.computeElapsedTime(timestamp);
+
+ WebInspector.frameResourceManager.mainFrame.markLoadEvent(computedTimestamp);
+
+ let eventMarker = new WebInspector.TimelineMarker(computedTimestamp, WebInspector.TimelineMarker.Type.LoadEvent);
+ this._activeRecording.addEventMarker(eventMarker);
+
+ this._stopAutoRecordingSoon();
+ }
+
+ memoryTrackingStart(timestamp)
+ {
+ // Called from WebInspector.MemoryObserver.
+
+ this.capturingStarted(timestamp);
+ }
+
+ memoryTrackingUpdate(event)
+ {
+ // Called from WebInspector.MemoryObserver.
+
+ if (!this._isCapturing)
+ return;
+
+ this._addRecord(new WebInspector.MemoryTimelineRecord(event.timestamp, event.categories));
+ }
+
+ memoryTrackingComplete()
+ {
+ // Called from WebInspector.MemoryObserver.
+ }
+
+ heapTrackingStarted(timestamp, snapshot)
+ {
+ // Called from WebInspector.HeapObserver.
+
+ this._addRecord(new WebInspector.HeapAllocationsTimelineRecord(timestamp, snapshot));
+
+ this.capturingStarted(timestamp);
+ }
+
+ heapTrackingCompleted(timestamp, snapshot)
+ {
+ // Called from WebInspector.HeapObserver.
+
+ this._addRecord(new WebInspector.HeapAllocationsTimelineRecord(timestamp, snapshot));
+ }
+
+ heapSnapshotAdded(timestamp, snapshot)
+ {
+ // Called from WebInspector.HeapAllocationsInstrument.
+
+ this._addRecord(new WebInspector.HeapAllocationsTimelineRecord(timestamp, snapshot));
+ }
+
+ // Private
+
+ _processRecord(recordPayload, parentRecordPayload)
+ {
+ var startTime = this.activeRecording.computeElapsedTime(recordPayload.startTime);
+ var endTime = this.activeRecording.computeElapsedTime(recordPayload.endTime);
+ var callFrames = this._callFramesFromPayload(recordPayload.stackTrace);
+
+ var significantCallFrame = null;
+ if (callFrames) {
+ for (var i = 0; i < callFrames.length; ++i) {
+ if (callFrames[i].nativeCode)
+ continue;
+ significantCallFrame = callFrames[i];
+ break;
+ }
+ }
+
+ var sourceCodeLocation = significantCallFrame && significantCallFrame.sourceCodeLocation;
+
+ switch (recordPayload.type) {
+ case TimelineAgent.EventType.ScheduleStyleRecalculation:
+ console.assert(isNaN(endTime));
+
+ // Pass the startTime as the endTime since this record type has no duration.
+ return new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.InvalidateStyles, startTime, startTime, callFrames, sourceCodeLocation);
+
+ case TimelineAgent.EventType.RecalculateStyles:
+ return new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.RecalculateStyles, startTime, endTime, callFrames, sourceCodeLocation);
+
+ case TimelineAgent.EventType.InvalidateLayout:
+ console.assert(isNaN(endTime));
+
+ // Pass the startTime as the endTime since this record type has no duration.
+ return new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.InvalidateLayout, startTime, startTime, callFrames, sourceCodeLocation);
+
+ case TimelineAgent.EventType.Layout:
+ var layoutRecordType = sourceCodeLocation ? WebInspector.LayoutTimelineRecord.EventType.ForcedLayout : WebInspector.LayoutTimelineRecord.EventType.Layout;
+ var quad = new WebInspector.Quad(recordPayload.data.root);
+ return new WebInspector.LayoutTimelineRecord(layoutRecordType, startTime, endTime, callFrames, sourceCodeLocation, quad);
+
+ case TimelineAgent.EventType.Paint:
+ var quad = new WebInspector.Quad(recordPayload.data.clip);
+ return new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.Paint, startTime, endTime, callFrames, sourceCodeLocation, quad);
+
+ case TimelineAgent.EventType.Composite:
+ return new WebInspector.LayoutTimelineRecord(WebInspector.LayoutTimelineRecord.EventType.Composite, startTime, endTime, callFrames, sourceCodeLocation);
+
+ case TimelineAgent.EventType.RenderingFrame:
+ if (!recordPayload.children || !recordPayload.children.length)
+ return null;
+
+ return new WebInspector.RenderingFrameTimelineRecord(startTime, endTime);
+
+ case TimelineAgent.EventType.EvaluateScript:
+ if (!sourceCodeLocation) {
+ var mainFrame = WebInspector.frameResourceManager.mainFrame;
+ var scriptResource = mainFrame.url === recordPayload.data.url ? mainFrame.mainResource : mainFrame.resourceForURL(recordPayload.data.url, true);
+ if (scriptResource) {
+ // The lineNumber is 1-based, but we expect 0-based.
+ var lineNumber = recordPayload.data.lineNumber - 1;
+
+ // FIXME: No column number is provided.
+ sourceCodeLocation = scriptResource.createSourceCodeLocation(lineNumber, 0);
+ }
+ }
+
+ var profileData = recordPayload.data.profile;
+
+ var record;
+ switch (parentRecordPayload && parentRecordPayload.type) {
+ case TimelineAgent.EventType.TimerFire:
+ record = new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.TimerFired, startTime, endTime, callFrames, sourceCodeLocation, parentRecordPayload.data.timerId, profileData);
+ break;
+ default:
+ record = new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.ScriptEvaluated, startTime, endTime, callFrames, sourceCodeLocation, null, profileData);
+ break;
+ }
+
+ this._webTimelineScriptRecordsExpectingScriptProfilerEvents.push(record);
+ return record;
+
+ case TimelineAgent.EventType.ConsoleProfile:
+ var profileData = recordPayload.data.profile;
+ // COMPATIBILITY (iOS 9): With the Sampling Profiler, profiles no longer include legacy profile data.
+ console.assert(profileData || TimelineAgent.setInstruments);
+ return new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.ConsoleProfileRecorded, startTime, endTime, callFrames, sourceCodeLocation, recordPayload.data.title, profileData);
+
+ case TimelineAgent.EventType.TimerFire:
+ case TimelineAgent.EventType.EventDispatch:
+ case TimelineAgent.EventType.FireAnimationFrame:
+ // These are handled when the parent of FunctionCall or EvaluateScript.
+ break;
+
+ case TimelineAgent.EventType.FunctionCall:
+ // FunctionCall always happens as a child of another record, and since the FunctionCall record
+ // has useful info we just make the timeline record here (combining the data from both records).
+ if (!parentRecordPayload) {
+ console.warn("Unexpectedly received a FunctionCall timeline record without a parent record");
+ break;
+ }
+
+ if (!sourceCodeLocation) {
+ var mainFrame = WebInspector.frameResourceManager.mainFrame;
+ var scriptResource = mainFrame.url === recordPayload.data.scriptName ? mainFrame.mainResource : mainFrame.resourceForURL(recordPayload.data.scriptName, true);
+ if (scriptResource) {
+ // The lineNumber is 1-based, but we expect 0-based.
+ var lineNumber = recordPayload.data.scriptLine - 1;
+
+ // FIXME: No column number is provided.
+ sourceCodeLocation = scriptResource.createSourceCodeLocation(lineNumber, 0);
+ }
+ }
+
+ var profileData = recordPayload.data.profile;
+
+ var record;
+ switch (parentRecordPayload.type) {
+ case TimelineAgent.EventType.TimerFire:
+ record = new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.TimerFired, startTime, endTime, callFrames, sourceCodeLocation, parentRecordPayload.data.timerId, profileData);
+ break;
+ case TimelineAgent.EventType.EventDispatch:
+ record = new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.EventDispatched, startTime, endTime, callFrames, sourceCodeLocation, parentRecordPayload.data.type, profileData);
+ break;
+ case TimelineAgent.EventType.FireAnimationFrame:
+ record = new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.AnimationFrameFired, startTime, endTime, callFrames, sourceCodeLocation, parentRecordPayload.data.id, profileData);
+ break;
+ case TimelineAgent.EventType.FunctionCall:
+ record = new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.ScriptEvaluated, startTime, endTime, callFrames, sourceCodeLocation, parentRecordPayload.data.id, profileData);
+ break;
+ case TimelineAgent.EventType.RenderingFrame:
+ record = new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.ScriptEvaluated, startTime, endTime, callFrames, sourceCodeLocation, parentRecordPayload.data.id, profileData);
+ break;
+
+ default:
+ console.assert(false, "Missed FunctionCall embedded inside of: " + parentRecordPayload.type);
+ break;
+ }
+
+ if (record) {
+ this._webTimelineScriptRecordsExpectingScriptProfilerEvents.push(record);
+ return record;
+ }
+ break;
+
+ case TimelineAgent.EventType.ProbeSample:
+ // Pass the startTime as the endTime since this record type has no duration.
+ sourceCodeLocation = WebInspector.probeManager.probeForIdentifier(recordPayload.data.probeId).breakpoint.sourceCodeLocation;
+ return new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.ProbeSampleRecorded, startTime, startTime, callFrames, sourceCodeLocation, recordPayload.data.probeId);
+
+ case TimelineAgent.EventType.TimerInstall:
+ console.assert(isNaN(endTime));
+
+ // Pass the startTime as the endTime since this record type has no duration.
+ var timerDetails = {timerId: recordPayload.data.timerId, timeout: recordPayload.data.timeout, repeating: !recordPayload.data.singleShot};
+ return new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.TimerInstalled, startTime, startTime, callFrames, sourceCodeLocation, timerDetails);
+
+ case TimelineAgent.EventType.TimerRemove:
+ console.assert(isNaN(endTime));
+
+ // Pass the startTime as the endTime since this record type has no duration.
+ return new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.TimerRemoved, startTime, startTime, callFrames, sourceCodeLocation, recordPayload.data.timerId);
+
+ case TimelineAgent.EventType.RequestAnimationFrame:
+ console.assert(isNaN(endTime));
+
+ // Pass the startTime as the endTime since this record type has no duration.
+ return new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.AnimationFrameRequested, startTime, startTime, callFrames, sourceCodeLocation, recordPayload.data.id);
+
+ case TimelineAgent.EventType.CancelAnimationFrame:
+ console.assert(isNaN(endTime));
+
+ // Pass the startTime as the endTime since this record type has no duration.
+ return new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.AnimationFrameCanceled, startTime, startTime, callFrames, sourceCodeLocation, recordPayload.data.id);
+
+ default:
+ console.error("Missing handling of Timeline Event Type: " + recordPayload.type);
+ }
+
+ return null;
+ }
+
+ _processEvent(recordPayload, parentRecordPayload)
+ {
+ switch (recordPayload.type) {
+ case TimelineAgent.EventType.TimeStamp:
+ var timestamp = this.activeRecording.computeElapsedTime(recordPayload.startTime);
+ var eventMarker = new WebInspector.TimelineMarker(timestamp, WebInspector.TimelineMarker.Type.TimeStamp, recordPayload.data.message);
+ this._activeRecording.addEventMarker(eventMarker);
+ break;
+
+ case TimelineAgent.EventType.Time:
+ case TimelineAgent.EventType.TimeEnd:
+ // FIXME: <https://webkit.org/b/150690> Web Inspector: Show console.time/timeEnd ranges in Timeline
+ // FIXME: Make use of "message" payload properties.
+ break;
+
+ default:
+ return this._processRecord(recordPayload, parentRecordPayload);
+ }
+
+ return null;
+ }
+
+ _loadNewRecording()
+ {
+ if (this._activeRecording && this._activeRecording.isEmpty())
+ return;
+
+ let instruments = this.enabledTimelineTypes.map((type) => WebInspector.Instrument.createForTimelineType(type));
+ let identifier = this._nextRecordingIdentifier++;
+ let newRecording = new WebInspector.TimelineRecording(identifier, WebInspector.UIString("Timeline Recording %d").format(identifier), instruments);
+
+ this._recordings.push(newRecording);
+ this.dispatchEventToListeners(WebInspector.TimelineManager.Event.RecordingCreated, {recording: newRecording});
+
+ if (this._isCapturing)
+ this.stopCapturing();
+
+ var oldRecording = this._activeRecording;
+ if (oldRecording)
+ oldRecording.unloaded();
+
+ this._activeRecording = newRecording;
+
+ // COMPATIBILITY (iOS 8): When using Legacy timestamps, a navigation will have computed
+ // the main resource's will send request timestamp in terms of the last page's base timestamp.
+ // Now that we have navigated, we should reset the legacy base timestamp and the
+ // will send request timestamp for the new main resource. This way, all new timeline
+ // records will be computed relative to the new navigation.
+ if (this._mainResourceForAutoCapturing && WebInspector.TimelineRecording.isLegacy) {
+ console.assert(this._mainResourceForAutoCapturing.originalRequestWillBeSentTimestamp);
+ this._activeRecording.setLegacyBaseTimestamp(this._mainResourceForAutoCapturing.originalRequestWillBeSentTimestamp);
+ this._mainResourceForAutoCapturing._requestSentTimestamp = 0;
+ }
+
+ this.dispatchEventToListeners(WebInspector.TimelineManager.Event.RecordingLoaded, {oldRecording});
+ }
+
+ _callFramesFromPayload(payload)
+ {
+ if (!payload)
+ return null;
+
+ return payload.map((x) => WebInspector.CallFrame.fromPayload(WebInspector.assumingMainTarget(), x));
+ }
+
+ _addRecord(record)
+ {
+ this._activeRecording.addRecord(record);
+
+ // Only worry about dead time after the load event.
+ if (WebInspector.frameResourceManager.mainFrame && isNaN(WebInspector.frameResourceManager.mainFrame.loadEventTimestamp))
+ this._resetAutoRecordingDeadTimeTimeout();
+ }
+
+ _attemptAutoCapturingForFrame(frame)
+ {
+ if (!this._autoCaptureOnPageLoad)
+ return false;
+
+ if (!frame.isMainFrame())
+ return false;
+
+ // COMPATIBILITY (iOS 9): Timeline.setAutoCaptureEnabled did not exist.
+ // Perform auto capture in the frontend.
+ if (!TimelineAgent.setAutoCaptureEnabled)
+ return this._legacyAttemptStartAutoCapturingForFrame(frame);
+
+ if (!this._shouldSetAutoCapturingMainResource)
+ return false;
+
+ console.assert(this._isCapturing, "We saw autoCaptureStarted so we should already be capturing");
+
+ let mainResource = frame.provisionalMainResource || frame.mainResource;
+ if (mainResource === this._mainResourceForAutoCapturing)
+ return false;
+
+ let oldMainResource = frame.mainResource || null;
+ this._isCapturingPageReload = oldMainResource !== null && oldMainResource.url === mainResource.url;
+
+ this._mainResourceForAutoCapturing = mainResource;
+
+ this._addRecord(new WebInspector.ResourceTimelineRecord(mainResource));
+
+ this._resetAutoRecordingMaxTimeTimeout();
+
+ this._shouldSetAutoCapturingMainResource = false;
+
+ return true;
+ }
+
+ _legacyAttemptStartAutoCapturingForFrame(frame)
+ {
+ if (this._isCapturing && !this._mainResourceForAutoCapturing)
+ return false;
+
+ let mainResource = frame.provisionalMainResource || frame.mainResource;
+ if (mainResource === this._mainResourceForAutoCapturing)
+ return false;
+
+ let oldMainResource = frame.mainResource || null;
+ this._isCapturingPageReload = oldMainResource !== null && oldMainResource.url === mainResource.url;
+
+ if (this._isCapturing)
+ this.stopCapturing();
+
+ this._mainResourceForAutoCapturing = mainResource;
+
+ this._loadNewRecording();
+
+ this.startCapturing();
+
+ this._addRecord(new WebInspector.ResourceTimelineRecord(mainResource));
+
+ this._resetAutoRecordingMaxTimeTimeout();
+
+ return true;
+ }
+
+ _stopAutoRecordingSoon()
+ {
+ // Only auto stop when auto capturing.
+ if (!this._isCapturing || !this._mainResourceForAutoCapturing)
+ return;
+
+ if (this._stopCapturingTimeout)
+ clearTimeout(this._stopCapturingTimeout);
+ this._stopCapturingTimeout = setTimeout(this._boundStopCapturing, WebInspector.TimelineManager.MaximumAutoRecordDurationAfterLoadEvent);
+ }
+
+ _resetAutoRecordingMaxTimeTimeout()
+ {
+ if (this._stopCapturingTimeout)
+ clearTimeout(this._stopCapturingTimeout);
+ this._stopCapturingTimeout = setTimeout(this._boundStopCapturing, WebInspector.TimelineManager.MaximumAutoRecordDuration);
+ }
+
+ _resetAutoRecordingDeadTimeTimeout()
+ {
+ // Only monitor dead time when auto capturing.
+ if (!this._isCapturing || !this._mainResourceForAutoCapturing)
+ return;
+
+ // Avoid unnecessary churning of timeout identifier by not tickling until 10ms have passed.
+ let now = Date.now();
+ if (now <= this._lastDeadTimeTickle)
+ return;
+ this._lastDeadTimeTickle = now + 10;
+
+ if (this._deadTimeTimeout)
+ clearTimeout(this._deadTimeTimeout);
+ this._deadTimeTimeout = setTimeout(this._boundStopCapturing, WebInspector.TimelineManager.DeadTimeRequiredToStopAutoRecordingEarly);
+ }
+
+ _provisionalLoadStarted(event)
+ {
+ this._attemptAutoCapturingForFrame(event.target);
+ }
+
+ _mainResourceDidChange(event)
+ {
+ let frame = event.target;
+ if (frame.isMainFrame() && WebInspector.settings.clearNetworkOnNavigate.value)
+ this._persistentNetworkTimeline.reset();
+
+ let mainResource = frame.mainResource;
+ let record = new WebInspector.ResourceTimelineRecord(mainResource);
+ if (!isNaN(record.startTime))
+ this._persistentNetworkTimeline.addRecord(record);
+
+ // Ignore resource events when there isn't a main frame yet. Those events are triggered by
+ // loading the cached resources when the inspector opens, and they do not have timing information.
+ if (!WebInspector.frameResourceManager.mainFrame)
+ return;
+
+ if (this._attemptAutoCapturingForFrame(frame))
+ return;
+
+ if (!this._isCapturing)
+ return;
+
+ if (mainResource === this._mainResourceForAutoCapturing)
+ return;
+
+ this._addRecord(record);
+ }
+
+ _resourceWasAdded(event)
+ {
+ var record = new WebInspector.ResourceTimelineRecord(event.data.resource);
+ if (!isNaN(record.startTime))
+ this._persistentNetworkTimeline.addRecord(record);
+
+ // Ignore resource events when there isn't a main frame yet. Those events are triggered by
+ // loading the cached resources when the inspector opens, and they do not have timing information.
+ if (!WebInspector.frameResourceManager.mainFrame)
+ return;
+
+ if (!this._isCapturing)
+ return;
+
+ this._addRecord(record);
+ }
+
+ _garbageCollected(event)
+ {
+ if (!this._isCapturing)
+ return;
+
+ let collection = event.data.collection;
+ this._addRecord(new WebInspector.ScriptTimelineRecord(WebInspector.ScriptTimelineRecord.EventType.GarbageCollected, collection.startTime, collection.endTime, null, null, collection));
+ }
+
+ _memoryPressure(event)
+ {
+ if (!this._isCapturing)
+ return;
+
+ this.activeRecording.addMemoryPressureEvent(event.data.memoryPressureEvent);
+ }
+
+ _scriptProfilerTypeToScriptTimelineRecordType(type)
+ {
+ switch (type) {
+ case ScriptProfilerAgent.EventType.API:
+ return WebInspector.ScriptTimelineRecord.EventType.APIScriptEvaluated;
+ case ScriptProfilerAgent.EventType.Microtask:
+ return WebInspector.ScriptTimelineRecord.EventType.MicrotaskDispatched;
+ case ScriptProfilerAgent.EventType.Other:
+ return WebInspector.ScriptTimelineRecord.EventType.ScriptEvaluated;
+ }
+ }
+
+ scriptProfilerProgrammaticCaptureStarted()
+ {
+ // FIXME: <https://webkit.org/b/158753> Generalize the concept of Instruments on the backend to work equally for JSContext and Web inspection
+ console.assert(WebInspector.debuggableType === WebInspector.DebuggableType.JavaScript);
+ console.assert(!this._isCapturing);
+
+ this.programmaticCaptureStarted();
+ }
+
+ scriptProfilerProgrammaticCaptureStopped()
+ {
+ // FIXME: <https://webkit.org/b/158753> Generalize the concept of Instruments on the backend to work equally for JSContext and Web inspection
+ console.assert(WebInspector.debuggableType === WebInspector.DebuggableType.JavaScript);
+ console.assert(this._isCapturing);
+
+ this.programmaticCaptureStopped();
+ }
+
+ scriptProfilerTrackingStarted(timestamp)
+ {
+ this._scriptProfilerRecords = [];
+
+ this.capturingStarted(timestamp);
+ }
+
+ scriptProfilerTrackingUpdated(event)
+ {
+ let {startTime, endTime, type} = event;
+ let scriptRecordType = this._scriptProfilerTypeToScriptTimelineRecordType(type);
+ let record = new WebInspector.ScriptTimelineRecord(scriptRecordType, startTime, endTime, null, null, null, null);
+ record.__scriptProfilerType = type;
+ this._scriptProfilerRecords.push(record);
+
+ // "Other" events, generated by Web content, will have wrapping Timeline records
+ // and need to be merged. Non-Other events, generated purely by the JavaScript
+ // engine or outside of the page via APIs, will not have wrapping Timeline
+ // records, so these records can just be added right now.
+ if (type !== ScriptProfilerAgent.EventType.Other)
+ this._addRecord(record);
+ }
+
+ scriptProfilerTrackingCompleted(samples)
+ {
+ console.assert(!this._webTimelineScriptRecordsExpectingScriptProfilerEvents || this._scriptProfilerRecords.length >= this._webTimelineScriptRecordsExpectingScriptProfilerEvents.length);
+
+ if (samples) {
+ let {stackTraces} = samples;
+ let topDownCallingContextTree = this.activeRecording.topDownCallingContextTree;
+ let bottomUpCallingContextTree = this.activeRecording.bottomUpCallingContextTree;
+ let topFunctionsTopDownCallingContextTree = this.activeRecording.topFunctionsTopDownCallingContextTree;
+ let topFunctionsBottomUpCallingContextTree = this.activeRecording.topFunctionsBottomUpCallingContextTree;
+
+ // Calculate a per-sample duration.
+ let timestampIndex = 0;
+ let timestampCount = stackTraces.length;
+ let sampleDurations = new Array(timestampCount);
+ let sampleDurationIndex = 0;
+ const defaultDuration = 1 / 1000; // 1ms.
+ for (let i = 0; i < this._scriptProfilerRecords.length; ++i) {
+ let record = this._scriptProfilerRecords[i];
+
+ // Use a default duration for timestamps recorded outside of ScriptProfiler events.
+ while (timestampIndex < timestampCount && stackTraces[timestampIndex].timestamp < record.startTime) {
+ sampleDurations[sampleDurationIndex++] = defaultDuration;
+ timestampIndex++;
+ }
+
+ // Average the duration per sample across all samples during the record.
+ let samplesInRecord = 0;
+ while (timestampIndex < timestampCount && stackTraces[timestampIndex].timestamp < record.endTime) {
+ timestampIndex++;
+ samplesInRecord++;
+ }
+ if (samplesInRecord) {
+ let averageDuration = (record.endTime - record.startTime) / samplesInRecord;
+ sampleDurations.fill(averageDuration, sampleDurationIndex, sampleDurationIndex + samplesInRecord);
+ sampleDurationIndex += samplesInRecord;
+ }
+ }
+
+ // Use a default duration for timestamps recorded outside of ScriptProfiler events.
+ if (timestampIndex < timestampCount)
+ sampleDurations.fill(defaultDuration, sampleDurationIndex);
+
+ for (let i = 0; i < stackTraces.length; i++) {
+ topDownCallingContextTree.updateTreeWithStackTrace(stackTraces[i], sampleDurations[i]);
+ bottomUpCallingContextTree.updateTreeWithStackTrace(stackTraces[i], sampleDurations[i]);
+ topFunctionsTopDownCallingContextTree.updateTreeWithStackTrace(stackTraces[i], sampleDurations[i]);
+ topFunctionsBottomUpCallingContextTree.updateTreeWithStackTrace(stackTraces[i], sampleDurations[i]);
+ }
+
+ // FIXME: This transformation should not be needed after introducing ProfileView.
+ // Once we eliminate ProfileNodeTreeElements and ProfileNodeDataGridNodes.
+ // <https://webkit.org/b/154973> Web Inspector: Timelines UI redesign: Remove TimelineSidebarPanel
+ for (let i = 0; i < this._scriptProfilerRecords.length; ++i) {
+ let record = this._scriptProfilerRecords[i];
+ record.profilePayload = topDownCallingContextTree.toCPUProfilePayload(record.startTime, record.endTime);
+ }
+ }
+
+ // Associate the ScriptProfiler created records with Web Timeline records.
+ // Filter out the already added ScriptProfiler events which should not have been wrapped.
+ if (WebInspector.debuggableType !== WebInspector.DebuggableType.JavaScript) {
+ this._scriptProfilerRecords = this._scriptProfilerRecords.filter((x) => x.__scriptProfilerType === ScriptProfilerAgent.EventType.Other);
+ this._mergeScriptProfileRecords();
+ }
+
+ this._scriptProfilerRecords = null;
+
+ let timeline = this.activeRecording.timelineForRecordType(WebInspector.TimelineRecord.Type.Script);
+ timeline.refresh();
+ }
+
+ _mergeScriptProfileRecords()
+ {
+ let nextRecord = function(list) { return list.shift() || null; };
+ let nextWebTimelineRecord = nextRecord.bind(null, this._webTimelineScriptRecordsExpectingScriptProfilerEvents);
+ let nextScriptProfilerRecord = nextRecord.bind(null, this._scriptProfilerRecords);
+ let recordEnclosesRecord = function(record1, record2) {
+ return record1.startTime <= record2.startTime && record1.endTime >= record2.endTime;
+ };
+
+ let webRecord = nextWebTimelineRecord();
+ let profilerRecord = nextScriptProfilerRecord();
+
+ while (webRecord && profilerRecord) {
+ // Skip web records with parent web records. For example an EvaluateScript with an EvaluateScript parent.
+ if (webRecord.parent instanceof WebInspector.ScriptTimelineRecord) {
+ console.assert(recordEnclosesRecord(webRecord.parent, webRecord), "Timeline Record incorrectly wrapping another Timeline Record");
+ webRecord = nextWebTimelineRecord();
+ continue;
+ }
+
+ // Normal case of a Web record wrapping a Script record.
+ if (recordEnclosesRecord(webRecord, profilerRecord)) {
+ webRecord.profilePayload = profilerRecord.profilePayload;
+ profilerRecord = nextScriptProfilerRecord();
+
+ // If there are more script profile records in the same time interval, add them
+ // as individual script evaluated records with profiles. This can happen with
+ // web microtask checkpoints that are technically inside of other web records.
+ // FIXME: <https://webkit.org/b/152903> Web Inspector: Timeline Cleanup: Better Timeline Record for Microtask Checkpoints
+ while (profilerRecord && recordEnclosesRecord(webRecord, profilerRecord)) {
+ this._addRecord(profilerRecord);
+ profilerRecord = nextScriptProfilerRecord();
+ }
+
+ webRecord = nextWebTimelineRecord();
+ continue;
+ }
+
+ // Profiler Record is entirely after the Web Record. This would mean an empty web record.
+ if (profilerRecord.startTime > webRecord.endTime) {
+ console.warn("Unexpected case of a Timeline record not containing a ScriptProfiler event and profile data");
+ webRecord = nextWebTimelineRecord();
+ continue;
+ }
+
+ // Non-wrapped profiler record.
+ console.warn("Unexpected case of a ScriptProfiler event not being contained by a Timeline record");
+ this._addRecord(profilerRecord);
+ profilerRecord = nextScriptProfilerRecord();
+ }
+
+ // Skipping the remaining ScriptProfiler events to match the current UI for handling Timeline records.
+ // However, the remaining ScriptProfiler records are valid and could be shown.
+ // FIXME: <https://webkit.org/b/152904> Web Inspector: Timeline UI should keep up with processing all incoming records
+ }
+
+ _updateAutoCaptureInstruments()
+ {
+ if (!window.TimelineAgent)
+ return;
+
+ if (!TimelineAgent.setInstruments)
+ return;
+
+ let instrumentSet = new Set;
+ let enabledTimelineTypes = this._enabledTimelineTypesSetting.value;
+
+ for (let timelineType of enabledTimelineTypes) {
+ switch (timelineType) {
+ case WebInspector.TimelineRecord.Type.Script:
+ instrumentSet.add(TimelineAgent.Instrument.ScriptProfiler);
+ break;
+ case WebInspector.TimelineRecord.Type.HeapAllocations:
+ instrumentSet.add(TimelineAgent.Instrument.Heap);
+ break;
+ case WebInspector.TimelineRecord.Type.Network:
+ case WebInspector.TimelineRecord.Type.RenderingFrame:
+ case WebInspector.TimelineRecord.Type.Layout:
+ instrumentSet.add(TimelineAgent.Instrument.Timeline);
+ break;
+ case WebInspector.TimelineRecord.Type.Memory:
+ instrumentSet.add(TimelineAgent.Instrument.Memory);
+ break;
+ }
+ }
+
+ TimelineAgent.setInstruments([...instrumentSet]);
+ }
+};
+
+WebInspector.TimelineManager.Event = {
+ RecordingCreated: "timeline-manager-recording-created",
+ RecordingLoaded: "timeline-manager-recording-loaded",
+ CapturingWillStart: "timeline-manager-capturing-will-start",
+ CapturingStarted: "timeline-manager-capturing-started",
+ CapturingStopped: "timeline-manager-capturing-stopped"
+};
+
+WebInspector.TimelineManager.MaximumAutoRecordDuration = 90000; // 90 seconds
+WebInspector.TimelineManager.MaximumAutoRecordDurationAfterLoadEvent = 10000; // 10 seconds
+WebInspector.TimelineManager.DeadTimeRequiredToStopAutoRecordingEarly = 2000; // 2 seconds