Web Inspector: Clicking a frame in the Rendering Frames timeline should select the...
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / TimelineRecordFrame.js
1 /*
2  * Copyright (C) 2015 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WebInspector.TimelineRecordFrame = function(graphDataSource, record)
27 {
28     // FIXME: Convert this to a WebInspector.Object subclass, and call super().
29     // WebInspector.Object.call(this);
30
31     this._element = document.createElement("div");
32     this._element.classList.add("timeline-record-frame");
33
34     this._graphDataSource = graphDataSource;
35     this._record = record || null;
36 };
37
38 // FIXME: Move to a WebInspector.Object subclass and we can remove this.
39 WebInspector.Object.deprecatedAddConstructorFunctions(WebInspector.TimelineRecordFrame);
40
41 WebInspector.TimelineRecordFrame.MinimumHeightPixels = 3;
42 WebInspector.TimelineRecordFrame.MaximumWidthPixels = 14;
43 WebInspector.TimelineRecordFrame.MinimumWidthPixels = 4;
44
45 WebInspector.TimelineRecordFrame.prototype = {
46     constructor: WebInspector.TimelineRecordFrame,
47     __proto__: WebInspector.Object.prototype,
48
49     // Public
50
51     get element()
52     {
53         return this._element;
54     },
55
56     get record()
57     {
58         return this._record;
59     },
60
61     set record(record)
62     {
63         this._record = record;
64     },
65
66     get selected()
67     {
68         return this._element.classList.contains("selected");
69     },
70
71     set selected(x)
72     {
73         if (this.selected === x)
74             return;
75
76         this._element.classList.toggle("selected");
77     },
78
79     refresh(graphDataSource)
80     {
81         if (!this._record)
82             return false;
83
84         var frameIndex = this._record.frameIndex;
85         var graphStartFrameIndex = Math.floor(graphDataSource.startTime);
86         var graphEndFrameIndex = graphDataSource.endTime;
87
88         // If this frame is completely before or after the bounds of the graph, return early.
89         if (frameIndex < graphStartFrameIndex || frameIndex > graphEndFrameIndex)
90             return false;
91
92         this._element.style.width = (1 / graphDataSource.timelineOverview.secondsPerPixel) + "px";
93
94         var graphDuration = graphDataSource.endTime - graphDataSource.startTime
95         var recordLeftPosition = (frameIndex - graphDataSource.startTime) / graphDuration;
96         this._updateElementPosition(this._element, recordLeftPosition, "left");
97         this._updateChildElements(graphDataSource);
98
99         return true;
100     },
101
102     // Private
103
104     _calculateFrameDisplayData(graphDataSource)
105     {
106         var secondsPerBlock = (graphDataSource.graphHeightSeconds / graphDataSource.element.offsetHeight) * WebInspector.TimelineRecordFrame.MinimumHeightPixels;
107         var segments = [];
108         var invisibleSegments = [];
109         var currentSegment = null;
110
111         function updateDurationRemainder(segment)
112         {
113             if (segment.duration <= secondsPerBlock) {
114                 segment.remainder = 0;
115                 return;
116             }
117
118             var roundedDuration = Math.roundTo(segment.duration, secondsPerBlock);
119             segment.remainder = Math.max(segment.duration - roundedDuration, 0);
120         }
121
122         function pushCurrentSegment()
123         {
124             updateDurationRemainder(currentSegment);
125             segments.push(currentSegment);
126             if (currentSegment.duration < secondsPerBlock)
127                 invisibleSegments.push({segment: currentSegment, index: segments.length - 1});
128
129             currentSegment = null;
130         }
131
132         // Frame segments aren't shown at arbitrary pixel heights, but are divided into blocks of pixels. One block
133         // represents the minimum displayable duration of a rendering frame, in seconds. Contiguous tasks less than a
134         // block high are grouped until the minimum is met, or a task meeting the minimum is found. The group is then
135         // added to the list of segment candidates. Large tasks (one block or more) are not grouped with other tasks
136         // and are simply added to the candidate list.
137         for (var key in WebInspector.RenderingFrameTimelineRecord.TaskType) {
138             var taskType = WebInspector.RenderingFrameTimelineRecord.TaskType[key];
139             var duration = this._record.durationForTask(taskType);
140             if (duration === 0)
141                 continue;
142
143             if (currentSegment && duration >= secondsPerBlock)
144                 pushCurrentSegment();
145
146             if (!currentSegment)
147                 currentSegment = {taskType: null, longestTaskDuration: 0, duration: 0, remainder: 0};
148
149             currentSegment.duration += duration;
150             if (duration > currentSegment.longestTaskDuration) {
151                 currentSegment.taskType = taskType;
152                 currentSegment.longestTaskDuration = duration;
153             }
154
155             if (currentSegment.duration >= secondsPerBlock)
156                 pushCurrentSegment();
157         }
158
159         if (currentSegment)
160             pushCurrentSegment();
161
162         // A frame consisting of a single segment is always visible.
163         if (segments.length === 1) {
164             segments[0].duration = Math.max(segments[0].duration, secondsPerBlock);
165             invisibleSegments = [];
166         }
167
168         // After grouping sub-block tasks, a second pass is needed to handle those groups that are still beneath the
169         // minimum displayable duration. Each sub-block task has one or two adjacent display segments greater than one
170         // block. The rounded-off time from these tasks is added to the sub-block, if it's sufficient to create a full
171         // block. Failing that, the task is merged with an adjacent segment.
172         invisibleSegments.sort(function(a, b) { return a.segment.duration - b.segment.duration; });
173
174         for (var item of invisibleSegments) {
175             var segment = item.segment;
176             var previousSegment = item.index > 0 ? segments[item.index - 1] : null;
177             var nextSegment = item.index < segments.length - 1 ? segments[item.index + 1] : null;
178             console.assert(previousSegment || nextSegment, "Invisible segment should have at least one adjacent visible segment.");
179
180             // Try to increase the segment's size to exactly one block, by taking subblock time from neighboring segments.
181             // If there are two neighbors, the one with greater subblock duration is borrowed from first.
182             var adjacentSegments;
183             var availableDuration;
184             if (previousSegment && nextSegment) {
185                 adjacentSegments = previousSegment.remainder > nextSegment.remainder ? [previousSegment, nextSegment] : [nextSegment, previousSegment];
186                 availableDuration = previousSegment.remainder + nextSegment.remainder;
187             } else {
188                 adjacentSegments = [previousSegment || nextSegment];
189                 availableDuration = adjacentSegments[0].remainder;
190             }
191
192             if (availableDuration < (secondsPerBlock - segment.duration)) {
193                 // Merge with largest adjacent segment.
194                 var targetSegment;
195                 if (previousSegment && nextSegment)
196                     targetSegment = previousSegment.duration > nextSegment.duration ? previousSegment : nextSegment;
197                 else
198                     targetSegment = previousSegment || nextSegment;
199
200                 targetSegment.duration += segment.duration;
201                 updateDurationRemainder(targetSegment);
202                 continue;
203             }
204
205             adjacentSegments.forEach(function(adjacentSegment) {
206                 if (segment.duration >= secondsPerBlock)
207                     return;
208                 var remainder = Math.min(secondsPerBlock - segment.duration, adjacentSegment.remainder);
209                 segment.duration += remainder;
210                 adjacentSegment.remainder -= remainder;
211             });
212         }
213
214         // Round visible segments to the nearest block, and compute the rounded frame duration.
215         var frameDuration = 0;
216         segments = segments.filter(function(segment) {
217             if (segment.duration < secondsPerBlock)
218                 return false;
219             segment.duration = Math.roundTo(segment.duration, secondsPerBlock);
220             frameDuration += segment.duration;
221             return true;
222         });
223
224         return {frameDuration, segments};
225     },
226
227     _updateChildElements(graphDataSource)
228     {
229         this._element.removeChildren();
230
231         console.assert(this._record);
232         if (!this._record)
233             return;
234
235         if (graphDataSource.graphHeightSeconds === 0)
236             return;
237
238         var frameElement = document.createElement("div");
239         frameElement.classList.add("frame");
240         this._element.appendChild(frameElement);
241
242         // Display data must be recalculated when the overview graph's vertical axis changes.
243         if (this._record.__displayData && this._record.__displayData.graphHeightSeconds !== graphDataSource.graphHeightSeconds)
244             this._record.__displayData = null;
245
246         if (!this._record.__displayData) {
247             this._record.__displayData = this._calculateFrameDisplayData(graphDataSource);
248             this._record.__displayData.graphHeightSeconds = graphDataSource.graphHeightSeconds;
249         }
250
251         var frameHeight = this._record.__displayData.frameDuration / graphDataSource.graphHeightSeconds;
252         if (frameHeight >= 0.95)
253             this._element.classList.add("tall");
254         else
255             this._element.classList.remove("tall");
256
257         this._updateElementPosition(frameElement, frameHeight, "height");
258
259         for (var segment of this._record.__displayData.segments) {
260             var element = document.createElement("div");
261             this._updateElementPosition(element, segment.duration / this._record.__displayData.frameDuration, "height");
262             element.classList.add("duration", segment.taskType);
263             frameElement.insertBefore(element, frameElement.firstChild);
264         }
265     },
266
267     _updateElementPosition(element, newPosition, property)
268     {
269         newPosition *= 100;
270         newPosition = newPosition.toFixed(2);
271
272         var currentPosition = parseFloat(element.style[property]).toFixed(2);
273         if (currentPosition !== newPosition)
274             element.style[property] = newPosition + "%";
275     }
276 };