/* * Copyright (C) 2013, 2015 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.TimelineOverview = class TimelineOverview extends WebInspector.View { constructor(timelineRecording) { super(); console.assert(timelineRecording instanceof WebInspector.TimelineRecording); this._timelinesViewModeSettings = this._createViewModeSettings(WebInspector.TimelineOverview.ViewMode.Timelines, 0.0001, 60, 0.01, 0, 15); if (WebInspector.FPSInstrument.supported()) { let minimumDurationPerPixel = 1 / WebInspector.TimelineRecordFrame.MaximumWidthPixels; let maximumDurationPerPixel = 1 / WebInspector.TimelineRecordFrame.MinimumWidthPixels; this._renderingFramesViewModeSettings = this._createViewModeSettings(WebInspector.TimelineOverview.ViewMode.RenderingFrames, minimumDurationPerPixel, maximumDurationPerPixel, minimumDurationPerPixel, 0, 100); } this._recording = timelineRecording; this._recording.addEventListener(WebInspector.TimelineRecording.Event.InstrumentAdded, this._instrumentAdded, this); this._recording.addEventListener(WebInspector.TimelineRecording.Event.InstrumentRemoved, this._instrumentRemoved, this); this._recording.addEventListener(WebInspector.TimelineRecording.Event.MarkerAdded, this._markerAdded, this); this._recording.addEventListener(WebInspector.TimelineRecording.Event.Reset, this._recordingReset, this); this.element.classList.add("timeline-overview"); this.element.addEventListener("wheel", this._handleWheelEvent.bind(this)); this.element.addEventListener("gesturestart", this._handleGestureStart.bind(this)); this.element.addEventListener("gesturechange", this._handleGestureChange.bind(this)); this.element.addEventListener("gestureend", this._handleGestureEnd.bind(this)); this._graphsContainerView = new WebInspector.View; this._graphsContainerView.element.classList.add("graphs-container"); this.addSubview(this._graphsContainerView); this._overviewGraphsByTypeMap = new Map; this._timelineRuler = new WebInspector.TimelineRuler; this._timelineRuler.allowsClippedLabels = true; this._timelineRuler.allowsTimeRangeSelection = true; this._timelineRuler.element.addEventListener("mousedown", this._timelineRulerMouseDown.bind(this)); this._timelineRuler.element.addEventListener("click", this._timelineRulerMouseClicked.bind(this)); this._timelineRuler.addEventListener(WebInspector.TimelineRuler.Event.TimeRangeSelectionChanged, this._timeRangeSelectionChanged, this); this.addSubview(this._timelineRuler); this._currentTimeMarker = new WebInspector.TimelineMarker(0, WebInspector.TimelineMarker.Type.CurrentTime); this._timelineRuler.addMarker(this._currentTimeMarker); this._scrollContainerElement = document.createElement("div"); this._scrollContainerElement.classList.add("scroll-container"); this._scrollContainerElement.addEventListener("scroll", this._handleScrollEvent.bind(this)); this.element.appendChild(this._scrollContainerElement); this._scrollWidthSizer = document.createElement("div"); this._scrollWidthSizer.classList.add("scroll-width-sizer"); this._scrollContainerElement.appendChild(this._scrollWidthSizer); this._startTime = 0; this._currentTime = 0; this._revealCurrentTime = false; this._endTime = 0; this._pixelAlignDuration = false; this._mouseWheelDelta = 0; this._cachedScrollContainerWidth = NaN; this._timelineRulerSelectionChanged = false; this._viewMode = WebInspector.TimelineOverview.ViewMode.Timelines; for (let instrument of this._recording.instruments) this._instrumentAdded(instrument); if (!WebInspector.timelineManager.isCapturingPageReload()) this._resetSelection(); this._viewModeDidChange(); } // Public get viewMode() { return this._viewMode; } set viewMode(x) { if (this._viewMode === x) return; this._viewMode = x; this._viewModeDidChange(); } get startTime() { return this._startTime; } set startTime(x) { x = x || 0; if (this._startTime === x) return; if (this._viewMode !== WebInspector.TimelineOverview.ViewMode.RenderingFrames) { let selectionOffset = this.selectionStartTime - this._startTime; this.selectionStartTime = selectionOffset + x; } this._startTime = x; this.needsLayout(); } get currentTime() { return this._currentTime; } set currentTime(x) { x = x || 0; if (this._currentTime === x) return; this._currentTime = x; this._revealCurrentTime = true; this.needsLayout(); } get secondsPerPixel() { return this._currentSettings.durationPerPixelSetting.value; } set secondsPerPixel(x) { x = Math.min(this._currentSettings.maximumDurationPerPixel, Math.max(this._currentSettings.minimumDurationPerPixel, x)); if (this.secondsPerPixel === x) return; if (this._pixelAlignDuration) { x = 1 / Math.round(1 / x); if (this.secondsPerPixel === x) return; } this._currentSettings.durationPerPixelSetting.value = x; this.needsLayout(); } get pixelAlignDuration() { return this._pixelAlignDuration; } set pixelAlignDuration(x) { if (this._pixelAlignDuration === x) return; this._mouseWheelDelta = 0; this._pixelAlignDuration = x; if (this._pixelAlignDuration) this.secondsPerPixel = 1 / Math.round(1 / this.secondsPerPixel); } get endTime() { return this._endTime; } set endTime(x) { x = x || 0; if (this._endTime === x) return; this._endTime = x; this.needsLayout(); } get scrollStartTime() { return this._currentSettings.scrollStartTime; } set scrollStartTime(x) { x = x || 0; if (this.scrollStartTime === x) return; this._currentSettings.scrollStartTime = x; this.needsLayout(); } get scrollContainerWidth() { return this._cachedScrollContainerWidth; } get visibleDuration() { if (isNaN(this._cachedScrollContainerWidth)) { this._cachedScrollContainerWidth = this._scrollContainerElement.offsetWidth; if (!this._cachedScrollContainerWidth) this._cachedScrollContainerWidth = NaN; } return this._cachedScrollContainerWidth * this.secondsPerPixel; } get selectionStartTime() { return this._timelineRuler.selectionStartTime; } set selectionStartTime(x) { x = x || 0; if (this._timelineRuler.selectionStartTime === x) return; let selectionDuration = this.selectionDuration; this._timelineRuler.selectionStartTime = x; this._timelineRuler.selectionEndTime = x + selectionDuration; } get selectionDuration() { return this._timelineRuler.selectionEndTime - this._timelineRuler.selectionStartTime; } set selectionDuration(x) { x = Math.max(this._timelineRuler.minimumSelectionDuration, x); this._timelineRuler.selectionEndTime = this._timelineRuler.selectionStartTime + x; } get height() { let height = 0; for (let overviewGraph of this._overviewGraphsByTypeMap.values()) { if (overviewGraph.visible) height += overviewGraph.height; } return height; } get visible() { return this._visible; } shown() { this._visible = true; for (let [type, overviewGraph] of this._overviewGraphsByTypeMap) { if (this._canShowTimelineType(type)) overviewGraph.shown(); } this.updateLayout(WebInspector.View.LayoutReason.Resize); } hidden() { this._visible = false; for (let overviewGraph of this._overviewGraphsByTypeMap.values()) overviewGraph.hidden(); } reset() { for (let overviewGraph of this._overviewGraphsByTypeMap.values()) overviewGraph.reset(); this._mouseWheelDelta = 0; this._resetSelection(); } revealMarker(marker) { this.scrollStartTime = marker.time - (this.visibleDuration / 2); } recordWasFiltered(timeline, record, filtered) { let overviewGraph = this._overviewGraphsByTypeMap.get(timeline.type); console.assert(overviewGraph, "Missing overview graph for timeline type " + timeline.type); if (!overviewGraph) return; console.assert(overviewGraph.visible, "Record filtered in hidden overview graph", record); overviewGraph.recordWasFiltered(record, filtered); } selectRecord(timeline, record) { let overviewGraph = this._overviewGraphsByTypeMap.get(timeline.type); console.assert(overviewGraph, "Missing overview graph for timeline type " + timeline.type); if (!overviewGraph) return; console.assert(overviewGraph.visible, "Record selected in hidden overview graph", record); overviewGraph.selectedRecord = record; } updateLayoutIfNeeded() { if (this.layoutPending) { super.updateLayoutIfNeeded(); return; } this._timelineRuler.updateLayoutIfNeeded(); for (let overviewGraph of this._overviewGraphsByTypeMap.values()) { if (overviewGraph.visible) overviewGraph.updateLayoutIfNeeded(); } } // Protected get timelineRuler() { return this._timelineRuler; } layout(layoutReason) { if (layoutReason === WebInspector.View.LayoutReason.Resize) this._cachedScrollContainerWidth = NaN; let startTime = this._startTime; let endTime = this._endTime; let currentTime = this._currentTime; if (this._viewMode === WebInspector.TimelineOverview.ViewMode.RenderingFrames) { let renderingFramesTimeline = this._recording.timelines.get(WebInspector.TimelineRecord.Type.RenderingFrame); console.assert(renderingFramesTimeline, "Recoring missing rendering frames timeline"); startTime = 0; endTime = renderingFramesTimeline.records.length; currentTime = endTime; } // Calculate the required width based on the duration and seconds per pixel. let duration = endTime - startTime; let newWidth = Math.ceil(duration / this.secondsPerPixel); // Update all relevant elements to the new required width. this._updateElementWidth(this._scrollWidthSizer, newWidth); this._currentTimeMarker.time = currentTime; if (this._revealCurrentTime) { this.revealMarker(this._currentTimeMarker); this._revealCurrentTime = false; } const visibleDuration = this.visibleDuration; // Clamp the scroll start time to match what the scroll bar would allow. let scrollStartTime = Math.min(this.scrollStartTime, endTime - visibleDuration); scrollStartTime = Math.max(startTime, scrollStartTime); this._timelineRuler.zeroTime = startTime; this._timelineRuler.startTime = scrollStartTime; this._timelineRuler.secondsPerPixel = this.secondsPerPixel; if (!this._dontUpdateScrollLeft) { this._ignoreNextScrollEvent = true; let scrollLeft = Math.ceil((scrollStartTime - startTime) / this.secondsPerPixel); if (scrollLeft) this._scrollContainerElement.scrollLeft = scrollLeft; } for (let overviewGraph of this._overviewGraphsByTypeMap.values()) { if (!overviewGraph.visible) continue; overviewGraph.zeroTime = startTime; overviewGraph.startTime = scrollStartTime; overviewGraph.currentTime = currentTime; overviewGraph.endTime = scrollStartTime + visibleDuration; } } // Private _updateElementWidth(element, newWidth) { var currentWidth = parseInt(element.style.width); if (currentWidth !== newWidth) element.style.width = newWidth + "px"; } _handleScrollEvent(event) { if (this._ignoreNextScrollEvent) { this._ignoreNextScrollEvent = false; return; } this._dontUpdateScrollLeft = true; let scrollOffset = this._scrollContainerElement.scrollLeft; this.scrollStartTime = this._startTime + (scrollOffset * this.secondsPerPixel); // Force layout so we can update with the scroll position synchronously. this.updateLayoutIfNeeded(); this._dontUpdateScrollLeft = false; } _handleWheelEvent(event) { // Ignore cloned events that come our way, we already handled the original. if (event.__cloned) return; // Ignore wheel events while handing gestures. if (this._handlingGesture) return; // Require twice the vertical delta to overcome horizontal scrolling. This prevents most // cases of inadvertent zooming for slightly diagonal scrolls. if (Math.abs(event.deltaX) >= Math.abs(event.deltaY) * 0.5) { // Clone the event to dispatch it on the scroll container. Mark it as cloned so we don't get into a loop. let newWheelEvent = new event.constructor(event.type, event); newWheelEvent.__cloned = true; this._scrollContainerElement.dispatchEvent(newWheelEvent); return; } // Remember the mouse position in time. let mouseOffset = event.pageX - this.element.totalOffsetLeft; let mousePositionTime = this._currentSettings.scrollStartTime + (mouseOffset * this.secondsPerPixel); let deviceDirection = event.webkitDirectionInvertedFromDevice ? 1 : -1; let delta = event.deltaY * (this.secondsPerPixel / WebInspector.TimelineOverview.ScrollDeltaDenominator) * deviceDirection; // Reset accumulated wheel delta when direction changes. if (this._pixelAlignDuration && (delta < 0 && this._mouseWheelDelta >= 0 || delta >= 0 && this._mouseWheelDelta < 0)) this._mouseWheelDelta = 0; let previousDurationPerPixel = this.secondsPerPixel; this._mouseWheelDelta += delta; this.secondsPerPixel += this._mouseWheelDelta; if (this.secondsPerPixel === this._currentSettings.minimumDurationPerPixel && delta < 0 || this.secondsPerPixel === this._currentSettings.maximumDurationPerPixel && delta >= 0) this._mouseWheelDelta = 0; else this._mouseWheelDelta = previousDurationPerPixel + this._mouseWheelDelta - this.secondsPerPixel; // Center the zoom around the mouse based on the remembered mouse position time. this.scrollStartTime = mousePositionTime - (mouseOffset * this.secondsPerPixel); event.preventDefault(); event.stopPropagation(); } _handleGestureStart(event) { if (this._handlingGesture) { // FIXME: [Mac] Unexpected gesturestart events when already handling gesture return; } let mouseOffset = event.pageX - this.element.totalOffsetLeft; let mousePositionTime = this._currentSettings.scrollStartTime + (mouseOffset * this.secondsPerPixel); this._handlingGesture = true; this._gestureStartStartTime = mousePositionTime; this._gestureStartDurationPerPixel = this.secondsPerPixel; event.preventDefault(); event.stopPropagation(); } _handleGestureChange(event) { // Cap zooming out at 5x. let scale = Math.max(1/5, event.scale); let mouseOffset = event.pageX - this.element.totalOffsetLeft; let newSecondsPerPixel = this._gestureStartDurationPerPixel / scale; this.secondsPerPixel = newSecondsPerPixel; this.scrollStartTime = this._gestureStartStartTime - (mouseOffset * this.secondsPerPixel); event.preventDefault(); event.stopPropagation(); } _handleGestureEnd(event) { this._handlingGesture = false; this._gestureStartStartTime = NaN; this._gestureStartDurationPerPixel = NaN; } _instrumentAdded(instrumentOrEvent) { let instrument = instrumentOrEvent instanceof WebInspector.Instrument ? instrumentOrEvent : instrumentOrEvent.data.instrument; console.assert(instrument instanceof WebInspector.Instrument, instrument); let timeline = this._recording.timelineForInstrument(instrument); console.assert(!this._overviewGraphsByTypeMap.has(timeline.type), timeline); let overviewGraph = WebInspector.TimelineOverviewGraph.createForTimeline(timeline, this); overviewGraph.addEventListener(WebInspector.TimelineOverviewGraph.Event.RecordSelected, this._recordSelected, this); this._overviewGraphsByTypeMap.set(timeline.type, overviewGraph); this._graphsContainerView.addSubview(overviewGraph); if (!this._canShowTimelineType(timeline.type)) overviewGraph.hidden(); } _instrumentRemoved(event) { let instrument = event.data.instrument; console.assert(instrument instanceof WebInspector.Instrument, instrument); let timeline = this._recording.timelineForInstrument(instrument); console.assert(this._overviewGraphsByTypeMap.has(timeline.type), timeline); let overviewGraph = this._overviewGraphsByTypeMap.take(timeline.type); overviewGraph.removeEventListener(WebInspector.TimelineOverviewGraph.Event.RecordSelected, this._recordSelected, this); this._graphsContainerView.removeSubview(overviewGraph); } _markerAdded(event) { this._timelineRuler.addMarker(event.data.marker); } _timelineRulerMouseDown(event) { this._timelineRulerSelectionChanged = false; } _timelineRulerMouseClicked(event) { if (this._timelineRulerSelectionChanged) return; for (let overviewGraph of this._overviewGraphsByTypeMap.values()) { let graphRect = overviewGraph.element.getBoundingClientRect(); if (!(event.pageX >= graphRect.left && event.pageX <= graphRect.right && event.pageY >= graphRect.top && event.pageY <= graphRect.bottom)) continue; // Clone the event to dispatch it on the overview graph element. let newClickEvent = new event.constructor(event.type, event); overviewGraph.element.dispatchEvent(newClickEvent); return; } } _timeRangeSelectionChanged(event) { this._timelineRulerSelectionChanged = true; let startTime = this._viewMode === WebInspector.TimelineOverview.ViewMode.Timelines ? this._startTime : 0; this._currentSettings.selectionStartValueSetting.value = this.selectionStartTime - startTime; this._currentSettings.selectionDurationSetting.value = this.selectionDuration; this.dispatchEventToListeners(WebInspector.TimelineOverview.Event.TimeRangeSelectionChanged); } _recordSelected(event) { for (let [type, overviewGraph] of this._overviewGraphsByTypeMap) { if (overviewGraph !== event.target) continue; let timeline = this._recording.timelines.get(type); console.assert(timeline, "Timeline recording missing timeline type", type); this.dispatchEventToListeners(WebInspector.TimelineOverview.Event.RecordSelected, {timeline, record: event.data.record}); return; } } _resetSelection() { function reset(settings) { settings.durationPerPixelSetting.reset(); settings.selectionStartValueSetting.reset(); settings.selectionDurationSetting.reset(); } reset(this._timelinesViewModeSettings); if (this._renderingFramesViewModeSettings) reset(this._renderingFramesViewModeSettings); this.secondsPerPixel = this._currentSettings.durationPerPixelSetting.value; this.selectionStartTime = this._currentSettings.selectionStartValueSetting.value; this.selectionDuration = this._currentSettings.selectionDurationSetting.value; } _recordingReset(event) { this._timelineRuler.clearMarkers(); this._timelineRuler.addMarker(this._currentTimeMarker); } _canShowTimelineType(type) { let timelineViewMode = WebInspector.TimelineOverview.ViewMode.Timelines; if (type === WebInspector.TimelineRecord.Type.RenderingFrame) timelineViewMode = WebInspector.TimelineOverview.ViewMode.RenderingFrames; return timelineViewMode === this._viewMode; } _viewModeDidChange() { let startTime = 0; let isRenderingFramesMode = this._viewMode === WebInspector.TimelineOverview.ViewMode.RenderingFrames; if (isRenderingFramesMode) { this._timelineRuler.minimumSelectionDuration = 1; this._timelineRuler.snapInterval = 1; this._timelineRuler.formatLabelCallback = (value) => value.toFixed(0); } else { this._timelineRuler.minimumSelectionDuration = 0.01; this._timelineRuler.snapInterval = NaN; this._timelineRuler.formatLabelCallback = null; startTime = this._startTime; } this.pixelAlignDuration = isRenderingFramesMode; this.selectionStartTime = this._currentSettings.selectionStartValueSetting.value + startTime; this.selectionDuration = this._currentSettings.selectionDurationSetting.value; for (let [type, overviewGraph] of this._overviewGraphsByTypeMap) { if (this._canShowTimelineType(type)) overviewGraph.shown(); else overviewGraph.hidden(); } this.element.classList.toggle("frames", isRenderingFramesMode); } _createViewModeSettings(viewMode, minimumDurationPerPixel, maximumDurationPerPixel, durationPerPixel, selectionStartValue, selectionDuration) { durationPerPixel = Math.min(maximumDurationPerPixel, Math.max(minimumDurationPerPixel, durationPerPixel)); let durationPerPixelSetting = new WebInspector.Setting(viewMode + "-duration-per-pixel", durationPerPixel); let selectionStartValueSetting = new WebInspector.Setting(viewMode + "-selection-start-value", selectionStartValue); let selectionDurationSetting = new WebInspector.Setting(viewMode + "-selection-duration", selectionDuration); return { scrollStartTime: 0, minimumDurationPerPixel, maximumDurationPerPixel, durationPerPixelSetting, selectionStartValueSetting, selectionDurationSetting }; } get _currentSettings() { return this._viewMode === WebInspector.TimelineOverview.ViewMode.Timelines ? this._timelinesViewModeSettings : this._renderingFramesViewModeSettings; } }; WebInspector.TimelineOverview.ScrollDeltaDenominator = 500; WebInspector.TimelineOverview.ViewMode = { Timelines: "timeline-overview-view-mode-timelines", RenderingFrames: "timeline-overview-view-mode-rendering-frames" }; WebInspector.TimelineOverview.Event = { RecordSelected: "timeline-overview-record-selected", TimeRangeSelectionChanged: "timeline-overview-time-range-selection-changed" };