diff options
Diffstat (limited to 'Source/WebInspectorUI/UserInterface/Views/TimelineRecordBar.js')
-rw-r--r-- | Source/WebInspectorUI/UserInterface/Views/TimelineRecordBar.js | 380 |
1 files changed, 380 insertions, 0 deletions
diff --git a/Source/WebInspectorUI/UserInterface/Views/TimelineRecordBar.js b/Source/WebInspectorUI/UserInterface/Views/TimelineRecordBar.js new file mode 100644 index 000000000..0f494c4a8 --- /dev/null +++ b/Source/WebInspectorUI/UserInterface/Views/TimelineRecordBar.js @@ -0,0 +1,380 @@ +/* + * Copyright (C) 2014, 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.TimelineRecordBar = class TimelineRecordBar extends WebInspector.Object +{ + constructor(records, renderMode) + { + super(); + + this._element = document.createElement("div"); + this._element.classList.add("timeline-record-bar"); + this._element[WebInspector.TimelineRecordBar.ElementReferenceSymbol] = this; + + this.renderMode = renderMode; + this.records = records; + } + + static createCombinedBars(records, secondsPerPixel, graphDataSource, createBarCallback) + { + if (!records.length) + return; + + var startTime = graphDataSource.startTime; + var currentTime = graphDataSource.currentTime; + var endTime = graphDataSource.endTime; + + var visibleRecords = []; + var usesActiveStartTime = false; + var lastRecordType = null; + + // FIXME: Do a binary search for records that fall inside start and current time. + + for (var i = 0; i < records.length; ++i) { + var record = records[i]; + if (isNaN(record.startTime)) + continue; + + // If this bar is completely before the bounds of the graph, skip this record. + if (record.endTime < startTime) + continue; + + // If this record is completely after the current time or end time, break out now. + // Records are sorted, so all records after this will be beyond the current or end time too. + if (record.startTime > currentTime || record.startTime > endTime) + break; + + if (record.usesActiveStartTime) + usesActiveStartTime = true; + + // If one record uses active time the rest are assumed to use it. + console.assert(record.usesActiveStartTime === usesActiveStartTime); + + // Only a single record type is supported right now. + console.assert(!lastRecordType || record.type === lastRecordType); + + visibleRecords.push(record); + + lastRecordType = record.type; + } + + if (!visibleRecords.length) + return; + + if (visibleRecords.length === 1) { + createBarCallback(visibleRecords, WebInspector.TimelineRecordBar.RenderMode.Normal); + return; + } + + function compareByActiveStartTime(a, b) + { + return a.activeStartTime - b.activeStartTime; + } + + var minimumDuration = secondsPerPixel * WebInspector.TimelineRecordBar.MinimumWidthPixels; + var minimumMargin = secondsPerPixel * WebInspector.TimelineRecordBar.MinimumMarginPixels; + + if (usesActiveStartTime) { + var inactiveStartTime = NaN; + var inactiveEndTime = NaN; + var inactiveRecords = []; + + for (var i = 0; i < visibleRecords.length; ++i) { + var record = visibleRecords[i]; + + // Check if the previous record is far enough away to create the inactive bar. + if (!isNaN(inactiveStartTime) && inactiveStartTime + Math.max(inactiveEndTime - inactiveStartTime, minimumDuration) + minimumMargin <= record.startTime) { + createBarCallback(inactiveRecords, WebInspector.TimelineRecordBar.RenderMode.InactiveOnly); + inactiveRecords = []; + inactiveStartTime = NaN; + inactiveEndTime = NaN; + } + + // If this is a new bar, peg the start time. + if (isNaN(inactiveStartTime)) + inactiveStartTime = record.startTime; + + // Update the end time to be the maximum we encounter. inactiveEndTime might be NaN, so "|| 0" to prevent Math.max from returning NaN. + inactiveEndTime = Math.max(inactiveEndTime || 0, record.activeStartTime); + + inactiveRecords.push(record); + } + + // Create the inactive bar for the last record if needed. + if (!isNaN(inactiveStartTime)) + createBarCallback(inactiveRecords, WebInspector.TimelineRecordBar.RenderMode.InactiveOnly); + + visibleRecords.sort(compareByActiveStartTime); + } + + var activeStartTime = NaN; + var activeEndTime = NaN; + var activeRecords = []; + + var startTimeProperty = usesActiveStartTime ? "activeStartTime" : "startTime"; + + for (var i = 0; i < visibleRecords.length; ++i) { + var record = visibleRecords[i]; + var startTime = record[startTimeProperty]; + + // Check if the previous record is far enough away to create the active bar. We also create it now if the current record has no active state time. + if (!isNaN(activeStartTime) && (activeStartTime + Math.max(activeEndTime - activeStartTime, minimumDuration) + minimumMargin <= startTime + || (isNaN(startTime) && !isNaN(activeEndTime)))) { + createBarCallback(activeRecords, WebInspector.TimelineRecordBar.RenderMode.ActiveOnly); + activeRecords = []; + activeStartTime = NaN; + activeEndTime = NaN; + } + + if (isNaN(startTime)) + continue; + + // If this is a new bar, peg the start time. + if (isNaN(activeStartTime)) + activeStartTime = startTime; + + // Update the end time to be the maximum we encounter. activeEndTime might be NaN, so "|| 0" to prevent Math.max from returning NaN. + if (!isNaN(record.endTime)) + activeEndTime = Math.max(activeEndTime || 0, record.endTime); + + activeRecords.push(record); + } + + // Create the active bar for the last record if needed. + if (!isNaN(activeStartTime)) + createBarCallback(activeRecords, WebInspector.TimelineRecordBar.RenderMode.ActiveOnly); + } + + static fromElement(element) + { + return element[WebInspector.TimelineRecordBar.ElementReferenceSymbol] || null; + } + + // Public + + get element() + { + return this._element; + } + + get renderMode() + { + return this._renderMode; + } + + set renderMode(renderMode) + { + this._renderMode = renderMode || WebInspector.TimelineRecordBar.RenderMode.Normal; + } + + get records() + { + return this._records; + } + + set records(records) + { + let oldRecordType; + let oldRecordEventType; + let oldRecordUsesActiveStartTime = false; + if (this._records && this._records.length) { + let oldRecord = this._records[0]; + oldRecordType = oldRecord.type; + oldRecordEventType = oldRecord.eventType; + oldRecordUsesActiveStartTime = oldRecord.usesActiveStartTime; + } + + records = records || []; + + this._records = records; + + // Assume all records in the group are the same type. + if (this._records.length) { + let newRecord = this._records[0]; + if (newRecord.type !== oldRecordType) { + this._element.classList.remove(oldRecordType); + this._element.classList.add(newRecord.type); + } + // Although all records may not have the same event type, the first record is + // sufficient to determine the correct style for the record bar. + if (newRecord.eventType !== oldRecordEventType) { + this._element.classList.remove(oldRecordEventType); + this._element.classList.add(newRecord.eventType); + } + if (newRecord.usesActiveStartTime !== oldRecordUsesActiveStartTime) + this._element.classList.toggle("has-inactive-segment", newRecord.usesActiveStartTime); + } else + this._element.classList.remove(oldRecordType, oldRecordEventType, "has-inactive-segment"); + } + + refresh(graphDataSource) + { + console.assert(graphDataSource.zeroTime); + console.assert(graphDataSource.startTime); + console.assert(graphDataSource.currentTime); + console.assert(graphDataSource.endTime); + console.assert(graphDataSource.secondsPerPixel); + + if (!this._records || !this._records.length) + return false; + + var firstRecord = this._records[0]; + var barStartTime = firstRecord.startTime; + + // If this bar has no time info, return early. + if (isNaN(barStartTime)) + return false; + + var graphStartTime = graphDataSource.startTime; + var graphEndTime = graphDataSource.endTime; + var graphCurrentTime = graphDataSource.currentTime; + + var barEndTime = this._records.reduce(function(previousValue, currentValue) { return Math.max(previousValue, currentValue.endTime); }, 0); + + // If this bar is completely after the current time, return early. + if (barStartTime > graphCurrentTime) + return false; + + // If this bar is completely before or after the bounds of the graph, return early. + if (barEndTime < graphStartTime || barStartTime > graphEndTime) + return false; + + var barUnfinished = isNaN(barEndTime) || barEndTime >= graphCurrentTime; + if (barUnfinished) + barEndTime = graphCurrentTime; + + var graphDuration = graphEndTime - graphStartTime; + + var newBarLeftPosition = (barStartTime - graphStartTime) / graphDuration; + this._updateElementPosition(this._element, newBarLeftPosition, "left"); + + var newBarWidth = ((barEndTime - graphStartTime) / graphDuration) - newBarLeftPosition; + this._updateElementPosition(this._element, newBarWidth, "width"); + + if (!this._activeBarElement && this._renderMode !== WebInspector.TimelineRecordBar.RenderMode.InactiveOnly) { + this._activeBarElement = document.createElement("div"); + this._activeBarElement.classList.add("segment"); + } + + if (!firstRecord.usesActiveStartTime) { + this._element.classList.toggle("unfinished", barUnfinished); + + if (this._inactiveBarElement) + this._inactiveBarElement.remove(); + + if (this._renderMode === WebInspector.TimelineRecordBar.RenderMode.InactiveOnly) { + if (this._activeBarElement) + this._activeBarElement.remove(); + + return false; + } + + // If this TimelineRecordBar is reused and had an inactive bar previously, clean it up. + this._activeBarElement.style.removeProperty("left"); + this._activeBarElement.style.removeProperty("width"); + + if (!this._activeBarElement.parentNode) + this._element.appendChild(this._activeBarElement); + + return true; + } + + // Find the earliest active start time for active only rendering, and the latest for the other modes. + // This matches the values that TimelineRecordBar.createCombinedBars uses when combining. + if (this._renderMode === WebInspector.TimelineRecordBar.RenderMode.ActiveOnly) + var barActiveStartTime = this._records.reduce(function(previousValue, currentValue) { return Math.min(previousValue, currentValue.activeStartTime); }, Infinity); + else + var barActiveStartTime = this._records.reduce(function(previousValue, currentValue) { return Math.max(previousValue, currentValue.activeStartTime); }, 0); + + var barDuration = barEndTime - barStartTime; + + var inactiveUnfinished = isNaN(barActiveStartTime) || barActiveStartTime >= graphCurrentTime; + this._element.classList.toggle("unfinished", inactiveUnfinished); + + if (inactiveUnfinished) + barActiveStartTime = graphCurrentTime; + else if (this._renderMode === WebInspector.TimelineRecordBar.RenderMode.Normal) { + // Hide the inactive segment when its duration is less than the minimum displayable size. + let minimumSegmentDuration = graphDataSource.secondsPerPixel * WebInspector.TimelineRecordBar.MinimumWidthPixels; + if (barActiveStartTime - barStartTime < minimumSegmentDuration) { + barActiveStartTime = barStartTime; + if (this._inactiveBarElement) + this._inactiveBarElement.remove(); + } + } + + let showInactiveSegment = barActiveStartTime > barStartTime; + this._element.classList.toggle("has-inactive-segment", showInactiveSegment); + + let middlePercentage = (barActiveStartTime - barStartTime) / barDuration; + if (showInactiveSegment && this._renderMode !== WebInspector.TimelineRecordBar.RenderMode.ActiveOnly) { + if (!this._inactiveBarElement) { + this._inactiveBarElement = document.createElement("div"); + this._inactiveBarElement.classList.add("segment"); + this._inactiveBarElement.classList.add("inactive"); + } + + this._updateElementPosition(this._inactiveBarElement, 1 - middlePercentage, "right"); + this._updateElementPosition(this._inactiveBarElement, middlePercentage, "width"); + + if (!this._inactiveBarElement.parentNode) + this._element.insertBefore(this._inactiveBarElement, this._element.firstChild); + } + + if (!inactiveUnfinished && this._renderMode !== WebInspector.TimelineRecordBar.RenderMode.InactiveOnly) { + this._updateElementPosition(this._activeBarElement, middlePercentage, "left"); + this._updateElementPosition(this._activeBarElement, 1 - middlePercentage, "width"); + + if (!this._activeBarElement.parentNode) + this._element.appendChild(this._activeBarElement); + } else if (this._activeBarElement) + this._activeBarElement.remove(); + + return true; + } + + // Private + + _updateElementPosition(element, newPosition, property) + { + newPosition *= 100; + + let newPositionAprox = Math.round(newPosition * 100); + let currentPositionAprox = Math.round(parseFloat(element.style[property]) * 100); + if (currentPositionAprox !== newPositionAprox) + element.style[property] = (newPositionAprox / 100) + "%"; + } +}; + +WebInspector.TimelineRecordBar.ElementReferenceSymbol = Symbol("timeline-record-bar"); + +WebInspector.TimelineRecordBar.MinimumWidthPixels = 4; +WebInspector.TimelineRecordBar.MinimumMarginPixels = 1; + +WebInspector.TimelineRecordBar.RenderMode = { + Normal: "timeline-record-bar-normal-render-mode", + InactiveOnly: "timeline-record-bar-inactive-only-render-mode", + ActiveOnly: "timeline-record-bar-active-only-render-mode" +}; |