| /* |
| * Copyright (C) 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. |
| */ |
| |
| WI.TimelineRecordFrame = class TimelineRecordFrame extends WI.Object |
| { |
| constructor(graphDataSource, record) |
| { |
| super(); |
| |
| this._element = document.createElement("div"); |
| this._element.classList.add("timeline-record-frame"); |
| |
| this._graphDataSource = graphDataSource; |
| this._record = record || null; |
| this._filtered = false; |
| } |
| |
| // Public |
| |
| get element() |
| { |
| return this._element; |
| } |
| |
| get record() |
| { |
| return this._record; |
| } |
| |
| set record(record) |
| { |
| this._record = record; |
| } |
| |
| get selected() |
| { |
| return this._element.classList.contains("selected"); |
| } |
| |
| set selected(x) |
| { |
| if (this.selected === x) |
| return; |
| |
| this._element.classList.toggle("selected"); |
| } |
| |
| get filtered() |
| { |
| return this._filtered; |
| } |
| |
| set filtered(x) |
| { |
| if (this._filtered === x) |
| return; |
| |
| this._filtered = x; |
| this._element.classList.toggle("filtered"); |
| } |
| |
| refresh(graphDataSource) |
| { |
| if (!this._record) |
| return false; |
| |
| var frameIndex = this._record.frameIndex; |
| var graphStartFrameIndex = Math.floor(graphDataSource.startTime); |
| var graphEndFrameIndex = graphDataSource.endTime; |
| |
| // If this frame is completely before or after the bounds of the graph, return early. |
| if (frameIndex < graphStartFrameIndex || frameIndex > graphEndFrameIndex) |
| return false; |
| |
| this._element.style.width = (1 / graphDataSource.timelineOverview.secondsPerPixel) + "px"; |
| |
| var graphDuration = graphDataSource.endTime - graphDataSource.startTime; |
| let recordPosition = (frameIndex - graphDataSource.startTime) / graphDuration; |
| |
| let property = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL ? "right" : "left"; |
| this._updateElementPosition(this._element, recordPosition, property); |
| |
| this._updateChildElements(graphDataSource); |
| |
| return true; |
| } |
| |
| // Private |
| |
| _calculateFrameDisplayData(graphDataSource) |
| { |
| var secondsPerBlock = (graphDataSource.graphHeightSeconds / graphDataSource.height) * WI.TimelineRecordFrame.MinimumHeightPixels; |
| var segments = []; |
| var invisibleSegments = []; |
| var currentSegment = null; |
| |
| function updateDurationRemainder(segment) |
| { |
| if (segment.duration <= secondsPerBlock) { |
| segment.remainder = 0; |
| return; |
| } |
| |
| var roundedDuration = Math.roundTo(segment.duration, secondsPerBlock); |
| segment.remainder = Math.max(segment.duration - roundedDuration, 0); |
| } |
| |
| function pushCurrentSegment() |
| { |
| updateDurationRemainder(currentSegment); |
| segments.push(currentSegment); |
| if (currentSegment.duration < secondsPerBlock) |
| invisibleSegments.push({segment: currentSegment, index: segments.length - 1}); |
| |
| currentSegment = null; |
| } |
| |
| // Frame segments aren't shown at arbitrary pixel heights, but are divided into blocks of pixels. One block |
| // represents the minimum displayable duration of a rendering frame, in seconds. Contiguous tasks less than a |
| // block high are grouped until the minimum is met, or a task meeting the minimum is found. The group is then |
| // added to the list of segment candidates. Large tasks (one block or more) are not grouped with other tasks |
| // and are simply added to the candidate list. |
| for (var key in WI.RenderingFrameTimelineRecord.TaskType) { |
| var taskType = WI.RenderingFrameTimelineRecord.TaskType[key]; |
| var duration = this._record.durationForTask(taskType); |
| if (duration === 0) |
| continue; |
| |
| if (currentSegment && duration >= secondsPerBlock) |
| pushCurrentSegment(); |
| |
| if (!currentSegment) |
| currentSegment = {taskType: null, longestTaskDuration: 0, duration: 0, remainder: 0}; |
| |
| currentSegment.duration += duration; |
| if (duration > currentSegment.longestTaskDuration) { |
| currentSegment.taskType = taskType; |
| currentSegment.longestTaskDuration = duration; |
| } |
| |
| if (currentSegment.duration >= secondsPerBlock) |
| pushCurrentSegment(); |
| } |
| |
| if (currentSegment) |
| pushCurrentSegment(); |
| |
| // A frame consisting of a single segment is always visible. |
| if (segments.length === 1) { |
| segments[0].duration = Math.max(segments[0].duration, secondsPerBlock); |
| invisibleSegments = []; |
| } |
| |
| // After grouping sub-block tasks, a second pass is needed to handle those groups that are still beneath the |
| // minimum displayable duration. Each sub-block task has one or two adjacent display segments greater than one |
| // block. The rounded-off time from these tasks is added to the sub-block, if it's sufficient to create a full |
| // block. Failing that, the task is merged with an adjacent segment. |
| invisibleSegments.sort(function(a, b) { return a.segment.duration - b.segment.duration; }); |
| |
| for (var item of invisibleSegments) { |
| var segment = item.segment; |
| var previousSegment = item.index > 0 ? segments[item.index - 1] : null; |
| var nextSegment = item.index < segments.length - 1 ? segments[item.index + 1] : null; |
| console.assert(previousSegment || nextSegment, "Invisible segment should have at least one adjacent visible segment."); |
| |
| // Try to increase the segment's size to exactly one block, by taking subblock time from neighboring segments. |
| // If there are two neighbors, the one with greater subblock duration is borrowed from first. |
| var adjacentSegments; |
| var availableDuration; |
| if (previousSegment && nextSegment) { |
| adjacentSegments = previousSegment.remainder > nextSegment.remainder ? [previousSegment, nextSegment] : [nextSegment, previousSegment]; |
| availableDuration = previousSegment.remainder + nextSegment.remainder; |
| } else { |
| adjacentSegments = [previousSegment || nextSegment]; |
| availableDuration = adjacentSegments[0].remainder; |
| } |
| |
| if (availableDuration < (secondsPerBlock - segment.duration)) { |
| // Merge with largest adjacent segment. |
| var targetSegment; |
| if (previousSegment && nextSegment) |
| targetSegment = previousSegment.duration > nextSegment.duration ? previousSegment : nextSegment; |
| else |
| targetSegment = previousSegment || nextSegment; |
| |
| targetSegment.duration += segment.duration; |
| updateDurationRemainder(targetSegment); |
| continue; |
| } |
| |
| adjacentSegments.forEach(function(adjacentSegment) { |
| if (segment.duration >= secondsPerBlock) |
| return; |
| var remainder = Math.min(secondsPerBlock - segment.duration, adjacentSegment.remainder); |
| segment.duration += remainder; |
| adjacentSegment.remainder -= remainder; |
| }); |
| } |
| |
| // Round visible segments to the nearest block, and compute the rounded frame duration. |
| var frameDuration = 0; |
| segments = segments.filter(function(segment) { |
| if (segment.duration < secondsPerBlock) |
| return false; |
| segment.duration = Math.roundTo(segment.duration, secondsPerBlock); |
| frameDuration += segment.duration; |
| return true; |
| }); |
| |
| return {frameDuration, segments}; |
| } |
| |
| _updateChildElements(graphDataSource) |
| { |
| this._element.removeChildren(); |
| |
| console.assert(this._record); |
| if (!this._record) |
| return; |
| |
| if (graphDataSource.graphHeightSeconds === 0) |
| return; |
| |
| var frameElement = document.createElement("div"); |
| frameElement.classList.add("frame"); |
| this._element.appendChild(frameElement); |
| |
| // Display data must be recalculated when the overview graph's vertical axis changes. |
| if (this._record.__displayData && this._record.__displayData.graphHeightSeconds !== graphDataSource.graphHeightSeconds) |
| this._record.__displayData = null; |
| |
| if (!this._record.__displayData) { |
| this._record.__displayData = this._calculateFrameDisplayData(graphDataSource); |
| this._record.__displayData.graphHeightSeconds = graphDataSource.graphHeightSeconds; |
| } |
| |
| var frameHeight = this._record.__displayData.frameDuration / graphDataSource.graphHeightSeconds; |
| if (frameHeight >= 0.95) |
| this._element.classList.add("tall"); |
| else |
| this._element.classList.remove("tall"); |
| |
| this._updateElementPosition(frameElement, frameHeight, "height"); |
| |
| for (var segment of this._record.__displayData.segments) { |
| var element = document.createElement("div"); |
| this._updateElementPosition(element, segment.duration / this._record.__displayData.frameDuration, "height"); |
| element.classList.add("duration", segment.taskType); |
| frameElement.insertBefore(element, frameElement.firstChild); |
| } |
| } |
| |
| _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) + "%"; |
| } |
| }; |
| |
| WI.TimelineRecordFrame.MinimumHeightPixels = 3; |
| WI.TimelineRecordFrame.MaximumWidthPixels = 14; |
| WI.TimelineRecordFrame.MinimumWidthPixels = 4; |