<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[186387] trunk/Source/WebInspectorUI</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.webkit.org/projects/webkit/changeset/186387">186387</a></dd>
<dt>Author</dt> <dd>mattbaker@apple.com</dd>
<dt>Date</dt> <dd>2015-07-06 17:23:04 -0700 (Mon, 06 Jul 2015)</dd>
</dl>

<h3>Log Message</h3>
<pre>Web Inspector: Rendering Frame bars appear misaligned and contain gaps when displaying small task segments
https://bugs.webkit.org/show_bug.cgi?id=146475

Reviewed by Timothy Hatcher.

Displaying all task segments within a frame in the Rendering Frames graph is impossible, as very short tasks
would result in a bar with a height of less than 1 pixel. Consecutive small tasks, each less than a visible
pixel, appear as gaps in the frame bar. This patch addresses these shortcomings by introducing a minimum
displayable frame height (3 pixels), and setting the height of every frame to a multiple of the minimum height.

* UserInterface/Base/Utilities.js:
(.value):
Added Math.roundTo to simplify rounding to arbitrary intervals.

* UserInterface/Views/TimelineRecordFrame.css:
(.timeline-record-frame):
(.timeline-record-frame &gt; .frame &gt; .duration):
Enforce 3px min height for frames &amp; segments. TimelineRenderingFrame's segment height calculator
creates segments that are always at least 3px, this is just a precaution.

* UserInterface/Views/TimelineRecordFrame.js:
(WebInspector.TimelineRecordFrame.prototype._calculateFrameDisplayData.updateDurationRemainder):
(WebInspector.TimelineRecordFrame.prototype._calculateFrameDisplayData.pushCurrentSegment):
(WebInspector.TimelineRecordFrame.prototype._calculateFrameDisplayData.invisibleSegments.forEach):
(WebInspector.TimelineRecordFrame.prototype._updateChildElements.createDurationElement): Deleted.
Added algorithm for calculating frame segment heights, rather than simply dividing each task's
duration by the frame duration. Results are cached so the segment heights aren't needlessly
recalculated on every scroll/zoom.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkSourceWebInspectorUIChangeLog">trunk/Source/WebInspectorUI/ChangeLog</a></li>
<li><a href="#trunkSourceWebInspectorUIUserInterfaceBaseUtilitiesjs">trunk/Source/WebInspectorUI/UserInterface/Base/Utilities.js</a></li>
<li><a href="#trunkSourceWebInspectorUIUserInterfaceViewsTimelineRecordFramecss">trunk/Source/WebInspectorUI/UserInterface/Views/TimelineRecordFrame.css</a></li>
<li><a href="#trunkSourceWebInspectorUIUserInterfaceViewsTimelineRecordFramejs">trunk/Source/WebInspectorUI/UserInterface/Views/TimelineRecordFrame.js</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkSourceWebInspectorUIChangeLog"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebInspectorUI/ChangeLog (186386 => 186387)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebInspectorUI/ChangeLog        2015-07-07 00:21:07 UTC (rev 186386)
+++ trunk/Source/WebInspectorUI/ChangeLog        2015-07-07 00:23:04 UTC (rev 186387)
</span><span class="lines">@@ -1,3 +1,34 @@
</span><ins>+2015-07-06  Matt Baker  &lt;mattbaker@apple.com&gt;
+
+        Web Inspector: Rendering Frame bars appear misaligned and contain gaps when displaying small task segments
+        https://bugs.webkit.org/show_bug.cgi?id=146475
+
+        Reviewed by Timothy Hatcher.
+
+        Displaying all task segments within a frame in the Rendering Frames graph is impossible, as very short tasks
+        would result in a bar with a height of less than 1 pixel. Consecutive small tasks, each less than a visible
+        pixel, appear as gaps in the frame bar. This patch addresses these shortcomings by introducing a minimum
+        displayable frame height (3 pixels), and setting the height of every frame to a multiple of the minimum height.
+
+        * UserInterface/Base/Utilities.js:
+        (.value):
+        Added Math.roundTo to simplify rounding to arbitrary intervals.
+
+        * UserInterface/Views/TimelineRecordFrame.css:
+        (.timeline-record-frame):
+        (.timeline-record-frame &gt; .frame &gt; .duration):
+        Enforce 3px min height for frames &amp; segments. TimelineRenderingFrame's segment height calculator
+        creates segments that are always at least 3px, this is just a precaution.
+
+        * UserInterface/Views/TimelineRecordFrame.js:
+        (WebInspector.TimelineRecordFrame.prototype._calculateFrameDisplayData.updateDurationRemainder):
+        (WebInspector.TimelineRecordFrame.prototype._calculateFrameDisplayData.pushCurrentSegment):
+        (WebInspector.TimelineRecordFrame.prototype._calculateFrameDisplayData.invisibleSegments.forEach):
+        (WebInspector.TimelineRecordFrame.prototype._updateChildElements.createDurationElement): Deleted.
+        Added algorithm for calculating frame segment heights, rather than simply dividing each task's
+        duration by the frame duration. Results are cached so the segment heights aren't needlessly
+        recalculated on every scroll/zoom.
+
</ins><span class="cx"> 2015-07-06  Timothy Hatcher  &lt;timothy@apple.com&gt;
</span><span class="cx"> 
</span><span class="cx">         Web Inspector: Force show Console tab when supportsSplitContentBrowser is false
</span></span></pre></div>
<a id="trunkSourceWebInspectorUIUserInterfaceBaseUtilitiesjs"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebInspectorUI/UserInterface/Base/Utilities.js (186386 => 186387)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebInspectorUI/UserInterface/Base/Utilities.js        2015-07-07 00:21:07 UTC (rev 186386)
+++ trunk/Source/WebInspectorUI/UserInterface/Base/Utilities.js        2015-07-07 00:23:04 UTC (rev 186387)
</span><span class="lines">@@ -797,6 +797,14 @@
</span><span class="cx">     }
</span><span class="cx"> });
</span><span class="cx"> 
</span><ins>+Object.defineProperty(Math, &quot;roundTo&quot;,
+{
+    value: function(num, step)
+    {
+        return Math.round(num / step) * step;
+    }
+});
+
</ins><span class="cx"> Object.defineProperty(Number, &quot;constrain&quot;,
</span><span class="cx"> {
</span><span class="cx">     value: function(num, min, max)
</span></span></pre></div>
<a id="trunkSourceWebInspectorUIUserInterfaceViewsTimelineRecordFramecss"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebInspectorUI/UserInterface/Views/TimelineRecordFrame.css (186386 => 186387)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebInspectorUI/UserInterface/Views/TimelineRecordFrame.css        2015-07-07 00:21:07 UTC (rev 186386)
+++ trunk/Source/WebInspectorUI/UserInterface/Views/TimelineRecordFrame.css        2015-07-07 00:23:04 UTC (rev 186387)
</span><span class="lines">@@ -26,6 +26,7 @@
</span><span class="cx"> .timeline-record-frame {
</span><span class="cx">     position: absolute;
</span><span class="cx">     height: 108px;
</span><ins>+    min-height: 3px;
</ins><span class="cx">     min-width: 4px;
</span><span class="cx">     width: 4px;
</span><span class="cx"> 
</span><span class="lines">@@ -49,6 +50,7 @@
</span><span class="cx"> .timeline-record-frame &gt; .frame &gt; .duration {
</span><span class="cx">     box-sizing: border-box;
</span><span class="cx"> 
</span><ins>+    min-height: 3px;
</ins><span class="cx">     background-color: rgb(221, 221, 221);
</span><span class="cx">     border-bottom: solid 1px rgb(245, 245, 245);
</span><span class="cx"> }
</span></span></pre></div>
<a id="trunkSourceWebInspectorUIUserInterfaceViewsTimelineRecordFramejs"></a>
<div class="modfile"><h4>Modified: trunk/Source/WebInspectorUI/UserInterface/Views/TimelineRecordFrame.js (186386 => 186387)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Source/WebInspectorUI/UserInterface/Views/TimelineRecordFrame.js        2015-07-07 00:21:07 UTC (rev 186386)
+++ trunk/Source/WebInspectorUI/UserInterface/Views/TimelineRecordFrame.js        2015-07-07 00:23:04 UTC (rev 186387)
</span><span class="lines">@@ -38,6 +38,7 @@
</span><span class="cx"> // FIXME: Move to a WebInspector.Object subclass and we can remove this.
</span><span class="cx"> WebInspector.Object.deprecatedAddConstructorFunctions(WebInspector.TimelineRecordFrame);
</span><span class="cx"> 
</span><ins>+WebInspector.TimelineRecordFrame.MinimumHeightPixels = 3;
</ins><span class="cx"> WebInspector.TimelineRecordFrame.MaximumWidthPixels = 14;
</span><span class="cx"> WebInspector.TimelineRecordFrame.MinimumWidthPixels = 4;
</span><span class="cx"> 
</span><span class="lines">@@ -87,6 +88,129 @@
</span><span class="cx"> 
</span><span class="cx">     // Private
</span><span class="cx"> 
</span><ins>+    _calculateFrameDisplayData(graphDataSource)
+    {
+        var secondsPerBlock = (graphDataSource.graphHeightSeconds / graphDataSource.element.offsetHeight) * WebInspector.TimelineRecordFrame.MinimumHeightPixels;
+        var segments = [];
+        var invisibleSegments = [];
+        var currentSegment = null;
+
+        function updateDurationRemainder(segment)
+        {
+            if (segment.duration &lt;= 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 &lt; 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 &amp;&amp; duration &gt;= secondsPerBlock)
+                pushCurrentSegment();
+
+            if (!currentSegment)
+                currentSegment = {taskType: null, longestTaskDuration: 0, duration: 0, remainder: 0};
+
+            currentSegment.duration += duration;
+            if (duration &gt; currentSegment.longestTaskDuration) {
+                currentSegment.taskType = taskType;
+                currentSegment.longestTaskDuration = duration;
+            }
+
+            if (currentSegment.duration &gt;= 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 &gt; 0 ? segments[item.index - 1] : null;
+            var nextSegment = item.index &lt; segments.length - 1 ? segments[item.index + 1] : null;
+            console.assert(previousSegment || nextSegment, &quot;Invisible segment should have at least one adjacent visible segment.&quot;);
+
+            // 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 &amp;&amp; nextSegment) {
+                adjacentSegments = previousSegment.remainder &gt; nextSegment.remainder ? [previousSegment, nextSegment] : [nextSegment, previousSegment];
+                availableDuration = previousSegment.remainder + nextSegment.remainder;
+            } else {
+                adjacentSegments = [previousSegment || nextSegment];
+                availableDuration = adjacentSegments[0].remainder;
+            }
+
+            if (availableDuration &lt; (secondsPerBlock - segment.duration)) {
+                // Merge with largest adjacent segment.
+                var targetSegment;
+                if (previousSegment &amp;&amp; nextSegment)
+                    targetSegment = previousSegment.duration &gt; nextSegment.duration ? previousSegment : nextSegment;
+                else
+                    targetSegment = previousSegment || nextSegment;
+
+                targetSegment.duration += segment.duration;
+                updateDurationRemainder(targetSegment);
+                continue;
+            }
+
+            adjacentSegments.forEach(function(adjacentSegment) {
+                if (segment.duration &gt;= 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 &lt; secondsPerBlock)
+                return false;
+            segment.duration = Math.roundTo(segment.duration, secondsPerBlock);
+            frameDuration += segment.duration;
+            return true;
+        });
+
+        return {frameDuration, segments};
+    },
+
</ins><span class="cx">     _updateChildElements(graphDataSource)
</span><span class="cx">     {
</span><span class="cx">         this._element.removeChildren();
</span><span class="lines">@@ -102,24 +226,23 @@
</span><span class="cx">         frameElement.classList.add(&quot;frame&quot;);
</span><span class="cx">         this._element.appendChild(frameElement);
</span><span class="cx"> 
</span><del>-        var frameHeight = this._record.duration / graphDataSource.graphHeightSeconds;
-        this._updateElementPosition(frameElement, frameHeight, &quot;height&quot;);
</del><ins>+        // Display data must be recalculated when the overview graph's vertical axis changes.
+        if (this._record.__displayData &amp;&amp; this._record.__displayData.graphHeightSeconds !== graphDataSource.graphHeightSeconds)
+            this._record.__displayData = null;
</ins><span class="cx"> 
</span><del>-        function createDurationElement(duration, taskType)
-        {
</del><ins>+        if (!this._record.__displayData) {
+            this._record.__displayData = this._calculateFrameDisplayData(graphDataSource);
+            this._record.__displayData.graphHeightSeconds = graphDataSource.graphHeightSeconds;
+        }
+
+        this._updateElementPosition(frameElement, this._record.__displayData.frameDuration / graphDataSource.graphHeightSeconds, &quot;height&quot;);
+
+        for (var segment of this._record.__displayData.segments) {
</ins><span class="cx">             var element = document.createElement(&quot;div&quot;);
</span><del>-            this._updateElementPosition(element, duration / this._record.duration, &quot;height&quot;);
-            element.classList.add(&quot;duration&quot;, taskType);
-            return element;
</del><ins>+            this._updateElementPosition(element, segment.duration / this._record.__displayData.frameDuration, &quot;height&quot;);
+            element.classList.add(&quot;duration&quot;, segment.taskType);
+            frameElement.insertBefore(element, frameElement.firstChild);
</ins><span class="cx">         }
</span><del>-
-        Object.keys(WebInspector.RenderingFrameTimelineRecord.TaskType).forEach(function(key) {
-            var taskType = WebInspector.RenderingFrameTimelineRecord.TaskType[key];
-            var duration = this._record.durationForTask(taskType);
-            if (duration === 0)
-                return;
-            frameElement.insertBefore(createDurationElement.call(this, duration, taskType), frameElement.firstChild);
-        }, this);
</del><span class="cx">     },
</span><span class="cx"> 
</span><span class="cx">     _updateElementPosition(element, newPosition, property)
</span></span></pre>
</div>
</div>

</body>
</html>