Web Inspector: CPU Usage Timeline - Allow clicking a bar in the overview to select...
authorjoepeck@webkit.org <joepeck@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 6 Mar 2019 22:28:30 +0000 (22:28 +0000)
committerjoepeck@webkit.org <joepeck@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 6 Mar 2019 22:28:30 +0000 (22:28 +0000)
https://bugs.webkit.org/show_bug.cgi?id=195321

Reviewed by Devin Rousso.

* UserInterface/Models/Timeline.js:
(WI.Timeline.prototype.closestRecordTo):
Helper to get the closest record to a timestamp.

* UserInterface/Views/CPUTimelineOverviewGraph.css:
(.timeline-overview-graph.cpu > .stacked-column-chart):
(.timeline-overview-graph.cpu > .stacked-column-chart > svg > rect.selected):
Style a selected record with the active color.

* UserInterface/Views/CPUTimelineOverviewGraph.js:
(WI.CPUTimelineOverviewGraph):
(WI.CPUTimelineOverviewGraph.prototype.get samplingRatePerSecond):
(WI.CPUTimelineOverviewGraph.prototype.reset):
(WI.CPUTimelineOverviewGraph.prototype.layout):
(WI.CPUTimelineOverviewGraph.prototype.updateSelectedRecord):
(WI.CPUTimelineOverviewGraph.prototype._graphPositionForMouseEvent):
(WI.CPUTimelineOverviewGraph.prototype._handleGraphMouseClick):
A click in the overview which hits a rect triggers a selection of
the associated timeline record.

* UserInterface/Views/StackedColumnChart.js:
(WI.StackedColumnChart.prototype.addColumnSet):
(WI.StackedColumnChart.prototype.layout):
Allow setting an additional class name with a column set.
It will set the class name on each rect in that column.

* UserInterface/Views/TimelineOverview.js:
(WI.TimelineOverview.prototype._recordSelected):
When selecting a CPU record, make a selection range of 2 neighboring
columns in each direction.

* UserInterface/Views/TimelineRuler.js:
(WI.TimelineRuler.prototype._handleClick):
When a sub-element has handled the click stop further event propagation.

* UserInterface/Views/TimelineOverviewGraph.js:
(WI.TimelineOverviewGraph.prototype.get selected):
Drive-by style fix.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@242567 268f45cc-cd09-0410-ab3c-d52691b4dbfc

Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/UserInterface/Models/Timeline.js
Source/WebInspectorUI/UserInterface/Views/CPUTimelineOverviewGraph.css
Source/WebInspectorUI/UserInterface/Views/CPUTimelineOverviewGraph.js
Source/WebInspectorUI/UserInterface/Views/StackedColumnChart.js
Source/WebInspectorUI/UserInterface/Views/TimelineOverview.js
Source/WebInspectorUI/UserInterface/Views/TimelineOverviewGraph.js
Source/WebInspectorUI/UserInterface/Views/TimelineRecordBar.js
Source/WebInspectorUI/UserInterface/Views/TimelineRuler.js

index 76a67e4..3f86932 100644 (file)
@@ -1,5 +1,51 @@
 2019-03-06  Joseph Pecoraro  <pecoraro@apple.com>
 
+        Web Inspector: CPU Usage Timeline - Allow clicking a bar in the overview to select a tight time range around it
+        https://bugs.webkit.org/show_bug.cgi?id=195321
+
+        Reviewed by Devin Rousso.
+
+        * UserInterface/Models/Timeline.js:
+        (WI.Timeline.prototype.closestRecordTo):
+        Helper to get the closest record to a timestamp.
+
+        * UserInterface/Views/CPUTimelineOverviewGraph.css:
+        (.timeline-overview-graph.cpu > .stacked-column-chart):
+        (.timeline-overview-graph.cpu > .stacked-column-chart > svg > rect.selected):
+        Style a selected record with the active color.
+
+        * UserInterface/Views/CPUTimelineOverviewGraph.js:
+        (WI.CPUTimelineOverviewGraph):
+        (WI.CPUTimelineOverviewGraph.prototype.get samplingRatePerSecond):
+        (WI.CPUTimelineOverviewGraph.prototype.reset):
+        (WI.CPUTimelineOverviewGraph.prototype.layout):
+        (WI.CPUTimelineOverviewGraph.prototype.updateSelectedRecord):
+        (WI.CPUTimelineOverviewGraph.prototype._graphPositionForMouseEvent):
+        (WI.CPUTimelineOverviewGraph.prototype._handleGraphMouseClick):
+        A click in the overview which hits a rect triggers a selection of
+        the associated timeline record.
+
+        * UserInterface/Views/StackedColumnChart.js:
+        (WI.StackedColumnChart.prototype.addColumnSet):
+        (WI.StackedColumnChart.prototype.layout):
+        Allow setting an additional class name with a column set.
+        It will set the class name on each rect in that column.
+
+        * UserInterface/Views/TimelineOverview.js:
+        (WI.TimelineOverview.prototype._recordSelected):
+        When selecting a CPU record, make a selection range of 2 neighboring
+        columns in each direction.
+
+        * UserInterface/Views/TimelineRuler.js:
+        (WI.TimelineRuler.prototype._handleClick):
+        When a sub-element has handled the click stop further event propagation.
+
+        * UserInterface/Views/TimelineOverviewGraph.js:
+        (WI.TimelineOverviewGraph.prototype.get selected):
+        Drive-by style fix.
+
+2019-03-06  Joseph Pecoraro  <pecoraro@apple.com>
+
         Web Inspector: TimelineOverview clicks do not always behave as expected
         https://bugs.webkit.org/show_bug.cgi?id=195319
 
index 54c3f6a..0be7ec5 100644 (file)
@@ -93,6 +93,24 @@ WI.Timeline = class Timeline extends WI.Object
         this.dispatchEventToListeners(WI.Timeline.Event.Refreshed);
     }
 
+    closestRecordTo(timestamp)
+    {
+        let lowerIndex = this._records.lowerBound(timestamp, (time, record) => time - record.endTime);
+
+        let recordBefore = this._records[lowerIndex - 1];
+        let recordAfter = this._records[lowerIndex];
+        if (!recordBefore && !recordAfter)
+            return null;
+        if (!recordBefore && recordAfter)
+            return recordAfter;
+        if (!recordAfter && recordBefore)
+            return recordBefore;
+
+        let before = Math.abs(recordBefore.endTime - timestamp);
+        let after = Math.abs(recordAfter.startTime - timestamp);
+        return (before < after) ? recordBefore : recordAfter;
+    }
+
     recordsOverlappingTimeRange(startTime, endTime)
     {
         let lowerIndex = this._records.lowerBound(startTime, (time, record) => time - record.endTime);
index 28fac76..688c76c 100644 (file)
@@ -74,6 +74,13 @@ body[dir=rtl] .timeline-overview-graph.cpu > .stacked-column-chart {
     fill: var(--cpu-worker-thread-fill-color);
 }
 
+.timeline-overview-graph.cpu > .stacked-column-chart > svg > rect.selected {
+    fill: var(--selected-background-color) !important;
+    fill-opacity: 0.5;
+    stroke: var(--selected-background-color-active) !important;
+    stroke-opacity: 0.8;
+}
+
 /* LegacyCPUTimeline */
 .timeline-overview-graph.cpu > .column-chart > svg > rect {
     stroke: var(--cpu-stroke-color);
index 2b2dd78..972b2e2 100644 (file)
@@ -46,12 +46,24 @@ WI.CPUTimelineOverviewGraph = class CPUTimelineOverviewGraph extends WI.Timeline
         this.addSubview(this._chart);
         this.element.appendChild(this._chart.element);
 
+        this._chart.element.addEventListener("click", this._handleChartClick.bind(this));
+
         this._legendElement = this.element.appendChild(document.createElement("div"));
         this._legendElement.classList.add("legend");
 
+        this._lastSelectedRecordInLayout = null;
+
         this.reset();
     }
 
+    // Static
+
+    static get samplingRatePerSecond()
+    {
+        // 500ms. This matches the ResourceUsageThread sampling frequency in the backend.
+        return 0.5;
+    }
+
     // Protected
 
     get height()
@@ -65,6 +77,7 @@ WI.CPUTimelineOverviewGraph = class CPUTimelineOverviewGraph extends WI.Timeline
 
         this._maxUsage = 0;
         this._cachedMaxUsage = undefined;
+        this._lastSelectedRecordInLayout = null;
 
         this._updateLegend();
         this._chart.clear();
@@ -83,6 +96,8 @@ WI.CPUTimelineOverviewGraph = class CPUTimelineOverviewGraph extends WI.Timeline
         if (isNaN(graphWidth))
             return;
 
+        this._lastSelectedRecordInLayout = this.selectedRecord;
+
         if (this._chart.size.width !== graphWidth || this._chart.size.height !== this.height)
             this._chart.size = new WI.Size(graphWidth, this.height);
 
@@ -91,9 +106,6 @@ WI.CPUTimelineOverviewGraph = class CPUTimelineOverviewGraph extends WI.Timeline
         let secondsPerPixel = this.timelineOverview.secondsPerPixel;
         let maxCapacity = Math.max(20, this._maxUsage * 1.05); // Add 5% for padding.
 
-        // 500ms. This matches the ResourceUsageThread sampling frequency in the backend.
-        const samplingRatePerSecond = 0.5;
-
         function xScale(time) {
             return (time - graphStartTime) / secondsPerPixel;
         }
@@ -104,7 +116,7 @@ WI.CPUTimelineOverviewGraph = class CPUTimelineOverviewGraph extends WI.Timeline
         }
 
         const includeRecordBeforeStart = true;
-        let visibleRecords = this._cpuTimeline.recordsInTimeRange(graphStartTime, visibleEndTime + (samplingRatePerSecond / 2), includeRecordBeforeStart);
+        let visibleRecords = this._cpuTimeline.recordsInTimeRange(graphStartTime, visibleEndTime + (CPUTimelineOverviewGraph.samplingRatePerSecond / 2), includeRecordBeforeStart);
         if (!visibleRecords.length)
             return;
 
@@ -112,23 +124,36 @@ WI.CPUTimelineOverviewGraph = class CPUTimelineOverviewGraph extends WI.Timeline
             return yScale(record.usage);
         }
 
-        let intervalWidth = (samplingRatePerSecond / secondsPerPixel);
+        let intervalWidth = (CPUTimelineOverviewGraph.samplingRatePerSecond / secondsPerPixel);
         const minimumDisplayHeight = 4;
 
         // Bars for each record.
         for (let record of visibleRecords) {
             let w = intervalWidth;
             let h3 = Math.max(minimumDisplayHeight, yScale(record.usage));
-            let x = xScale(record.startTime - (samplingRatePerSecond / 2));
+            let x = xScale(record.startTime - (CPUTimelineOverviewGraph.samplingRatePerSecond / 2));
             if (WI.settings.experimentalEnableCPUUsageEnhancements.value) {
+                let additionalClass = record === this.selectedRecord ? "selected" : undefined;
                 let h1 = Math.max(minimumDisplayHeight, yScale(record.mainThreadUsage));
                 let h2 = Math.max(minimumDisplayHeight, yScale(record.mainThreadUsage + record.workerThreadUsage));
-                this._chart.addColumnSet(x, height, w, [h1, h2, h3]);
+                this._chart.addColumnSet(x, height, w, [h1, h2, h3], additionalClass);
             } else
                 this._chart.addColumn(x, height - h3, w, h3);
         }
     }
 
+    updateSelectedRecord()
+    {
+        super.updateSelectedRecord();
+
+        if (this._lastSelectedRecordInLayout !== this.selectedRecord) {
+            // Since we don't have the exact element to re-style with a selected appearance
+            // we trigger another layout to re-layout the graph and provide additional
+            // styles for the column for the selected record.
+            this.needsLayout();
+        }
+    }
+
     // Private
 
     _updateLegend()
@@ -147,6 +172,48 @@ WI.CPUTimelineOverviewGraph = class CPUTimelineOverviewGraph extends WI.Timeline
         }
     }
 
+    _graphPositionForMouseEvent(event)
+    {
+        // Only trigger if clicking on a rect, not anywhere in the graph.
+        let elements = document.elementsFromPoint(event.pageX, event.pageY);
+        let rectElement = elements.find((x) => x.localName === "rect");
+        if (!rectElement)
+            return NaN;
+
+        let chartElement = rectElement.closest(".stacked-column-chart");
+        if (!chartElement)
+            return NaN;
+
+        let rect = chartElement.getBoundingClientRect();
+        let position = event.pageX - rect.left;
+
+        if (WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL)
+            return rect.width - position;
+        return position;
+    }
+
+    _handleChartClick(event)
+    {
+        let position = this._graphPositionForMouseEvent(event);
+        if (isNaN(position))
+            return;
+
+        let secondsPerPixel = this.timelineOverview.secondsPerPixel;
+        let graphClickTime = position * secondsPerPixel;
+        let graphStartTime = this.startTime;
+
+        let clickTime = graphStartTime + graphClickTime;
+        let record = this._cpuTimeline.closestRecordTo(clickTime);
+        if (!record)
+            return;
+
+        // Ensure that the container "click" listener added by `WI.TimelineOverview` isn't called.
+        event.__timelineRecordClickEventHandled = true;
+
+        this.selectedRecord = record;
+        this.needsLayout();
+    }
+
     _cpuTimelineRecordAdded(event)
     {
         let cpuTimelineRecord = event.data.record;
index f76ef5c..b2f302c 100644 (file)
@@ -84,11 +84,11 @@ WI.StackedColumnChart = class StackedColumnChart extends WI.View
         this._sections = sectionClassNames;
     }
 
-    addColumnSet(x, totalHeight, width, heights)
+    addColumnSet(x, totalHeight, width, heights, additionalClass)
     {
         console.assert(heights.length === this._sections.length, "Wrong number of sections in columns set", heights.length, this._sections.length);
 
-        this._columns.push({x, totalHeight, width, heights});
+        this._columns.push({x, totalHeight, width, heights, additionalClass});
     }
 
     clear()
@@ -107,7 +107,7 @@ WI.StackedColumnChart = class StackedColumnChart extends WI.View
 
         this._svgElement.removeChildren();
 
-        for (let {x, totalHeight, width, heights} of this._columns) {
+        for (let {x, totalHeight, width, heights, additionalClass} of this._columns) {
             for (let i = heights.length - 1; i >= 0; --i) {
                 let height = heights[i];
                 // Next rect will be identical, skip this one.
@@ -116,6 +116,8 @@ WI.StackedColumnChart = class StackedColumnChart extends WI.View
                 let y = totalHeight - height;
                 let rect = this._svgElement.appendChild(createSVGElement("rect"));
                 rect.classList.add(this._sections[i]);
+                if (additionalClass)
+                    rect.classList.add(additionalClass);
                 rect.setAttribute("width", width);
                 rect.setAttribute("height", height);
                 rect.setAttribute("x", x);
index 33b0aea..57536b7 100644 (file)
@@ -688,7 +688,7 @@ WI.TimelineOverview = class TimelineOverview extends WI.View
     _handleGraphsContainerClick(event)
     {
         // Set when a WI.TimelineRecordBar receives the "click" first and is about to be selected.
-        if (event.__timelineRecordBarClick)
+        if (event.__timelineRecordClickEventHandled)
             return;
 
         this._recordSelected(null, null);
@@ -766,7 +766,11 @@ WI.TimelineOverview = class TimelineOverview extends WI.View
             let startTime = firstRecord instanceof WI.RenderingFrameTimelineRecord ? firstRecord.frameIndex : firstRecord.startTime;
             let endTime = lastRecord instanceof WI.RenderingFrameTimelineRecord ? lastRecord.frameIndex : lastRecord.endTime;
 
-            if (startTime < this.selectionStartTime || endTime > this.selectionStartTime + this.selectionDuration) {
+            if (firstRecord instanceof WI.CPUTimelineRecord) {
+                let selectionPadding = WI.CPUTimelineOverviewGraph.samplingRatePerSecond * 2.25;
+                this.selectionStartTime = startTime - selectionPadding;
+                this.selectionDuration = endTime - startTime + (selectionPadding * 2);
+            } else if (startTime < this.selectionStartTime || endTime > this.selectionStartTime + this.selectionDuration) {
                 let selectionPadding = this.secondsPerPixel * 10;
                 this.selectionStartTime = startTime - selectionPadding;
                 this.selectionDuration = endTime - startTime + (selectionPadding * 2);
index 317353a..a94f47f 100644 (file)
@@ -204,7 +204,10 @@ WI.TimelineOverviewGraph = class TimelineOverviewGraph extends WI.View
         return 36;
     }
 
-    get selected() { return this._selected; }
+    get selected()
+    {
+        return this._selected;
+    }
 
     set selected(x)
     {
index 0dc2d62..02786b9 100644 (file)
@@ -389,7 +389,7 @@ WI.TimelineRecordBar = class TimelineRecordBar extends WI.Object
     _handleClick(event)
     {
         // Ensure that the container "click" listener added by `WI.TimelineOverview` isn't called.
-        event.__timelineRecordBarClick = true;
+        event.__timelineRecordClickEventHandled = true;
 
         if (this._delegate.timelineRecordBarClicked)
             this._delegate.timelineRecordBarClicked(this);
index d0110e1..aa254b4 100644 (file)
@@ -774,7 +774,10 @@ WI.TimelineRuler = class TimelineRuler extends WI.View
                 continue;
 
             // Clone the event to dispatch it on the new element.
-            newTarget.dispatchEvent(new event.constructor(event.type, event));
+            let newEvent = new event.constructor(event.type, event);
+            newTarget.dispatchEvent(newEvent);
+            if (newEvent.__timelineRecordClickEventHandled)
+                event.stop();
             return;
         }
     }