blob: cf54013cec9a2d727badf5dc96b8ee62e3953db1 [file] [log] [blame]
/*
* Copyright (C) 2013 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.
*/
WI.TimelineRecording = class TimelineRecording extends WI.Object
{
constructor(identifier, displayName, instruments)
{
super();
this._identifier = identifier;
this._timelines = new Map;
this._displayName = displayName;
this._capturing = false;
this._readonly = false;
this._imported = false;
this._instruments = instruments || [];
this._startTime = NaN;
this._endTime = NaN;
this._discontinuityStartTime = NaN;
this._discontinuities = null;
this._firstRecordOfTypeAfterDiscontinuity = new Set;
this._exportDataRecords = null;
this._exportDataMarkers = null;
this._exportDataMemoryPressureEvents = null;
this._exportDataSampleStackTraces = null;
this._exportDataSampleDurations = null;
this._topDownCallingContextTree = new WI.CallingContextTree(WI.CallingContextTree.Type.TopDown);
this._bottomUpCallingContextTree = new WI.CallingContextTree(WI.CallingContextTree.Type.BottomUp);
this._topFunctionsTopDownCallingContextTree = new WI.CallingContextTree(WI.CallingContextTree.Type.TopFunctionsTopDown);
this._topFunctionsBottomUpCallingContextTree = new WI.CallingContextTree(WI.CallingContextTree.Type.TopFunctionsBottomUp);
for (let type of WI.TimelineManager.availableTimelineTypes()) {
let timeline = WI.Timeline.create(type);
this._timelines.set(type, timeline);
timeline.addEventListener(WI.Timeline.Event.TimesUpdated, this._timelineTimesUpdated, this);
}
// For legacy backends, we compute the elapsed time of records relative to this timestamp.
this._legacyFirstRecordedTimestamp = NaN;
this.reset(true);
}
// Static
static sourceCodeTimelinesSupported()
{
// FIXME: Support Network Timeline in ServiceWorker.
return WI.sharedApp.isWebDebuggable();
}
// Import / Export
static async import(identifier, json, displayName)
{
let {startTime, endTime, discontinuities, instrumentTypes, records, markers, memoryPressureEvents, sampleStackTraces, sampleDurations} = json;
let importedDisplayName = WI.UIString("Imported - %s").format(displayName);
let instruments = instrumentTypes.map((type) => WI.Instrument.createForTimelineType(type));
let recording = new WI.TimelineRecording(identifier, importedDisplayName, instruments);
recording._readonly = true;
recording._imported = true;
recording._startTime = startTime;
recording._endTime = endTime;
recording._discontinuities = discontinuities;
recording.initializeCallingContextTrees(sampleStackTraces, sampleDurations);
for (let recordJSON of records) {
let record = await WI.TimelineRecord.fromJSON(recordJSON);
if (record) {
recording.addRecord(record);
if (record instanceof WI.ScriptTimelineRecord)
record.profilePayload = recording._topDownCallingContextTree.toCPUProfilePayload(record.startTime, record.endTime);
}
}
for (let memoryPressureJSON of memoryPressureEvents) {
let memoryPressureEvent = WI.MemoryPressureEvent.fromJSON(memoryPressureJSON);
if (memoryPressureEvent)
recording.addMemoryPressureEvent(memoryPressureEvent);
}
// Add markers once we've transitioned the active recording.
setTimeout(() => {
recording.__importing = true;
for (let markerJSON of markers) {
let marker = WI.TimelineMarker.fromJSON(markerJSON);
if (marker)
recording.addEventMarker(marker);
}
recording.__importing = false;
});
return recording;
}
exportData()
{
console.assert(this.canExport(), "Attempted to export a recording which should not be exportable.");
// FIXME: Overview data (sourceCodeTimelinesMap).
// FIXME: Record hierarchy (parent / child relationship) is lost.
return {
displayName: this._displayName,
startTime: this._startTime,
endTime: this._endTime,
discontinuities: this._discontinuities,
instrumentTypes: this._instruments.map((instrument) => instrument.timelineRecordType),
records: this._exportDataRecords,
markers: this._exportDataMarkers,
memoryPressureEvents: this._exportDataMemoryPressureEvents,
sampleStackTraces: this._exportDataSampleStackTraces,
sampleDurations: this._exportDataSampleDurations,
};
}
// Public
get displayName() { return this._displayName; }
get identifier() { return this._identifier; }
get timelines() { return this._timelines; }
get instruments() { return this._instruments; }
get capturing() { return this._capturing; }
get readonly() { return this._readonly; }
get imported() { return this._imported; }
get startTime() { return this._startTime; }
get endTime() { return this._endTime; }
get topDownCallingContextTree() { return this._topDownCallingContextTree; }
get bottomUpCallingContextTree() { return this._bottomUpCallingContextTree; }
get topFunctionsTopDownCallingContextTree() { return this._topFunctionsTopDownCallingContextTree; }
get topFunctionsBottomUpCallingContextTree() { return this._topFunctionsBottomUpCallingContextTree; }
start(initiatedByBackend)
{
console.assert(!this._capturing, "Attempted to start an already started session.");
console.assert(!this._readonly, "Attempted to start a readonly session.");
this._capturing = true;
for (let instrument of this._instruments)
instrument.startInstrumentation(initiatedByBackend);
if (!isNaN(this._discontinuityStartTime)) {
for (let instrument of this._instruments)
this._firstRecordOfTypeAfterDiscontinuity.add(instrument.timelineRecordType);
}
}
stop(initiatedByBackend)
{
console.assert(this._capturing, "Attempted to stop an already stopped session.");
console.assert(!this._readonly, "Attempted to stop a readonly session.");
this._capturing = false;
for (let instrument of this._instruments)
instrument.stopInstrumentation(initiatedByBackend);
}
capturingStarted(startTime)
{
// A discontinuity occurs when the recording is stopped and resumed at
// a future time. Capturing started signals the end of the current
// discontinuity, if one exists.
if (!isNaN(this._discontinuityStartTime)) {
this._discontinuities.push({
startTime: this._discontinuityStartTime,
endTime: startTime,
});
this._discontinuityStartTime = NaN;
}
}
capturingStopped(endTime)
{
this._discontinuityStartTime = endTime;
}
saveIdentityToCookie()
{
// Do nothing. Timeline recordings are not persisted when the inspector is
// re-opened, so do not attempt to restore by identifier or display name.
}
isEmpty()
{
for (var timeline of this._timelines.values()) {
if (timeline.records.length)
return false;
}
return true;
}
unloaded(importing)
{
console.assert(importing || !this.isEmpty(), "Shouldn't unload an empty recording; it should be reused instead.");
this._readonly = true;
this.dispatchEventToListeners(WI.TimelineRecording.Event.Unloaded);
}
reset(suppressEvents)
{
console.assert(!this._readonly, "Can't reset a read-only recording.");
this._sourceCodeTimelinesMap = new Map;
this._startTime = NaN;
this._endTime = NaN;
this._discontinuityStartTime = NaN;
this._discontinuities = [];
this._firstRecordOfTypeAfterDiscontinuity.clear();
this._exportDataRecords = [];
this._exportDataMarkers = [];
this._exportDataMemoryPressureEvents = [];
this._exportDataSampleStackTraces = [];
this._exportDataSampleDurations = [];
this._topDownCallingContextTree.reset();
this._bottomUpCallingContextTree.reset();
this._topFunctionsTopDownCallingContextTree.reset();
this._topFunctionsBottomUpCallingContextTree.reset();
for (var timeline of this._timelines.values())
timeline.reset(suppressEvents);
WI.RenderingFrameTimelineRecord.resetFrameIndex();
if (!suppressEvents) {
this.dispatchEventToListeners(WI.TimelineRecording.Event.Reset);
this.dispatchEventToListeners(WI.TimelineRecording.Event.TimesUpdated);
}
}
get sourceCodeTimelines()
{
let timelines = [];
for (let timelinesForSourceCode of this._sourceCodeTimelinesMap.values())
timelines.pushAll(timelinesForSourceCode.values());
return timelines;
}
timelineForInstrument(instrument)
{
return this._timelines.get(instrument.timelineRecordType);
}
instrumentForTimeline(timeline)
{
return this._instruments.find((instrument) => instrument.timelineRecordType === timeline.type);
}
timelineForRecordType(recordType)
{
return this._timelines.get(recordType);
}
addInstrument(instrument)
{
console.assert(instrument instanceof WI.Instrument, instrument);
console.assert(!this._instruments.includes(instrument), this._instruments, instrument);
this._instruments.push(instrument);
this.dispatchEventToListeners(WI.TimelineRecording.Event.InstrumentAdded, {instrument});
}
removeInstrument(instrument)
{
console.assert(instrument instanceof WI.Instrument, instrument);
console.assert(this._instruments.includes(instrument), this._instruments, instrument);
this._instruments.remove(instrument);
this.dispatchEventToListeners(WI.TimelineRecording.Event.InstrumentRemoved, {instrument});
}
addEventMarker(marker)
{
this._exportDataMarkers.push(marker);
if (!this._capturing && !this.__importing)
return;
this.dispatchEventToListeners(WI.TimelineRecording.Event.MarkerAdded, {marker});
}
addRecord(record)
{
this._exportDataRecords.push(record);
let timeline = this._timelines.get(record.type);
console.assert(timeline, record, this._timelines);
if (!timeline)
return;
let discontinuity = null;
if (this._firstRecordOfTypeAfterDiscontinuity.take(record.type))
discontinuity = this._discontinuities.lastValue;
// Add the record to the global timeline by type.
timeline.addRecord(record, {discontinuity});
// Some records don't have source code timelines.
if (record.type === WI.TimelineRecord.Type.Network
|| record.type === WI.TimelineRecord.Type.RenderingFrame
|| record.type === WI.TimelineRecord.Type.CPU
|| record.type === WI.TimelineRecord.Type.Memory
|| record.type === WI.TimelineRecord.Type.HeapAllocations)
return;
if (!WI.TimelineRecording.sourceCodeTimelinesSupported())
return;
// Add the record to the source code timelines.
let sourceCode = null;
if (record.sourceCodeLocation)
sourceCode = record.sourceCodeLocation.sourceCode;
else if (record.type === WI.TimelineRecord.Type.Media) {
if (record.domNode && record.domNode.frame)
sourceCode = record.domNode.frame.mainResource;
}
if (!sourceCode)
sourceCode = WI.networkManager.mainFrame.provisionalMainResource || WI.networkManager.mainFrame.mainResource;
var sourceCodeTimelines = this._sourceCodeTimelinesMap.get(sourceCode);
if (!sourceCodeTimelines) {
sourceCodeTimelines = new Map;
this._sourceCodeTimelinesMap.set(sourceCode, sourceCodeTimelines);
}
var newTimeline = false;
var key = this._keyForRecord(record);
var sourceCodeTimeline = sourceCodeTimelines.get(key);
if (!sourceCodeTimeline) {
sourceCodeTimeline = new WI.SourceCodeTimeline(sourceCode, record.sourceCodeLocation, record.type, record.eventType);
sourceCodeTimelines.set(key, sourceCodeTimeline);
newTimeline = true;
}
sourceCodeTimeline.addRecord(record);
if (newTimeline)
this.dispatchEventToListeners(WI.TimelineRecording.Event.SourceCodeTimelineAdded, {sourceCodeTimeline});
}
addMemoryPressureEvent(memoryPressureEvent)
{
this._exportDataMemoryPressureEvents.push(memoryPressureEvent);
let memoryTimeline = this._timelines.get(WI.TimelineRecord.Type.Memory);
console.assert(memoryTimeline, this._timelines);
if (!memoryTimeline)
return;
memoryTimeline.addMemoryPressureEvent(memoryPressureEvent);
}
discontinuitiesInTimeRange(startTime, endTime)
{
return this._discontinuities.filter((item) => item.startTime <= endTime && item.endTime >= startTime);
}
addScriptInstrumentForProgrammaticCapture()
{
for (let instrument of this._instruments) {
if (instrument instanceof WI.ScriptInstrument)
return;
}
this.addInstrument(new WI.ScriptInstrument);
let instrumentTypes = this._instruments.map((instrument) => instrument.timelineRecordType);
WI.timelineManager.enabledTimelineTypes = instrumentTypes;
}
computeElapsedTime(timestamp)
{
if (!timestamp || isNaN(timestamp))
return NaN;
// COMPATIBILITY (iOS 8): old backends send timestamps (seconds or milliseconds since the epoch),
// rather than seconds elapsed since timeline capturing started. We approximate the latter by
// subtracting the start timestamp, as old versions did not use monotonic times.
if (WI.TimelineRecording.isLegacy === undefined)
WI.TimelineRecording.isLegacy = timestamp > WI.TimelineRecording.TimestampThresholdForLegacyRecordConversion;
if (!WI.TimelineRecording.isLegacy)
return timestamp;
// If the record's start time is large, but not really large, then it is seconds since epoch
// not millseconds since epoch, so convert it to milliseconds.
if (timestamp < WI.TimelineRecording.TimestampThresholdForLegacyAssumedMilliseconds)
timestamp *= 1000;
if (isNaN(this._legacyFirstRecordedTimestamp))
this._legacyFirstRecordedTimestamp = timestamp;
// Return seconds since the first recorded value.
return (timestamp - this._legacyFirstRecordedTimestamp) / 1000.0;
}
setLegacyBaseTimestamp(timestamp)
{
console.assert(isNaN(this._legacyFirstRecordedTimestamp));
if (timestamp < WI.TimelineRecording.TimestampThresholdForLegacyAssumedMilliseconds)
timestamp *= 1000;
this._legacyFirstRecordedTimestamp = timestamp;
}
initializeTimeBoundsIfNecessary(timestamp)
{
if (isNaN(this._startTime)) {
console.assert(isNaN(this._endTime));
this._startTime = timestamp;
this._endTime = timestamp;
this.dispatchEventToListeners(WI.TimelineRecording.Event.TimesUpdated);
}
}
initializeCallingContextTrees(stackTraces, sampleDurations)
{
this._exportDataSampleStackTraces.pushAll(stackTraces);
this._exportDataSampleDurations.pushAll(sampleDurations);
for (let i = 0; i < stackTraces.length; i++) {
this._topDownCallingContextTree.updateTreeWithStackTrace(stackTraces[i], sampleDurations[i]);
this._bottomUpCallingContextTree.updateTreeWithStackTrace(stackTraces[i], sampleDurations[i]);
this._topFunctionsTopDownCallingContextTree.updateTreeWithStackTrace(stackTraces[i], sampleDurations[i]);
this._topFunctionsBottomUpCallingContextTree.updateTreeWithStackTrace(stackTraces[i], sampleDurations[i]);
}
}
canExport()
{
if (this._capturing)
return false;
if (isNaN(this._startTime))
return false;
return true;
}
// Private
_keyForRecord(record)
{
var key = record.type;
if (record instanceof WI.ScriptTimelineRecord || record instanceof WI.LayoutTimelineRecord)
key += ":" + record.eventType;
if (record instanceof WI.ScriptTimelineRecord && record.eventType === WI.ScriptTimelineRecord.EventType.EventDispatched)
key += ":" + record.details;
if (record instanceof WI.MediaTimelineRecord) {
key += ":" + record.eventType;
if (record.eventType === WI.MediaTimelineRecord.EventType.DOMEvent) {
if (record.domEvent && record.domEvent.eventName)
key += ":" + record.domEvent.eventName;
} else if (record.eventType === WI.MediaTimelineRecord.EventType.PowerEfficientPlaybackStateChanged)
key += ":" + (record.isPowerEfficient ? "enabled" : "disabled");
}
if (record.sourceCodeLocation)
key += ":" + record.sourceCodeLocation.lineNumber + ":" + record.sourceCodeLocation.columnNumber;
return key;
}
_timelineTimesUpdated(event)
{
var timeline = event.target;
var changed = false;
if (isNaN(this._startTime) || timeline.startTime < this._startTime) {
this._startTime = timeline.startTime;
changed = true;
}
if (isNaN(this._endTime) || this._endTime < timeline.endTime) {
this._endTime = timeline.endTime;
changed = true;
}
if (changed)
this.dispatchEventToListeners(WI.TimelineRecording.Event.TimesUpdated);
}
};
WI.TimelineRecording.Event = {
Reset: "timeline-recording-reset",
Unloaded: "timeline-recording-unloaded",
SourceCodeTimelineAdded: "timeline-recording-source-code-timeline-added",
InstrumentAdded: "timeline-recording-instrument-added",
InstrumentRemoved: "timeline-recording-instrument-removed",
TimesUpdated: "timeline-recording-times-updated",
MarkerAdded: "timeline-recording-marker-added",
};
WI.TimelineRecording.isLegacy = undefined;
WI.TimelineRecording.TimestampThresholdForLegacyRecordConversion = 10000000; // Some value not near zero.
WI.TimelineRecording.TimestampThresholdForLegacyAssumedMilliseconds = 1420099200000; // Date.parse("Jan 1, 2015"). Milliseconds since epoch.
WI.TimelineRecording.SerializationVersion = 1;