/* * 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); } 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 || record.type === WI.TimelineRecord.Type.Screenshots) 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; return 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]); } } get exportMode() { return WI.FileUtilities.SaveMode.SingleFile; } canExport() { if (!WI.FileUtilities.canSave(this.exportMode)) return false; 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.SerializationVersion = 1;