blob: 78a40a306003ebce51edb7441a31fe5016abf0ae [file] [log] [blame]
/*
* 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;