/* * 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. */ WebInspector.TimelineRecordFrame = class TimelineRecordFrame extends WebInspector.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 var recordLeftPosition = (frameIndex - graphDataSource.startTime) / graphDuration; this._updateElementPosition(this._element, recordLeftPosition, "left"); this._updateChildElements(graphDataSource); return true; } // Private _calculateFrameDisplayData(graphDataSource) { var secondsPerBlock = (graphDataSource.graphHeightSeconds / graphDataSource.height) * WebInspector.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 WebInspector.RenderingFrameTimelineRecord.TaskType) { var taskType = WebInspector.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; newPosition = newPosition.toFixed(2); var currentPosition = parseFloat(element.style[property]).toFixed(2); if (currentPosition !== newPosition) element.style[property] = newPosition + "%"; } }; WebInspector.TimelineRecordFrame.MinimumHeightPixels = 3; WebInspector.TimelineRecordFrame.MaximumWidthPixels = 14; WebInspector.TimelineRecordFrame.MinimumWidthPixels = 4;