Web Inspector: CPU Usage - Energy Impact Section
authorjoepeck@webkit.org <joepeck@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sat, 2 Mar 2019 01:18:13 +0000 (01:18 +0000)
committerjoepeck@webkit.org <joepeck@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sat, 2 Mar 2019 01:18:13 +0000 (01:18 +0000)
https://bugs.webkit.org/show_bug.cgi?id=195151

Reviewed by Devin Rousso.

* Localizations/en.lproj/localizedStrings.js:
* UserInterface/Main.html:
New strings and resources.

* UserInterface/Views/CPUTimelineView.css:
(.timeline-view.cpu > .content .subtitle > .info):
(@media (prefers-color-scheme: dark)):
(.energy-info-popover-content):
(.timeline-view.cpu > .content > .overview > .divider):
(body[dir=ltr] .timeline-view.cpu > .content > .overview > .divider):
(body[dir=rtl] .timeline-view.cpu > .content > .overview > .divider):
(.timeline-view.cpu :matches(.area-chart, .stacked-area-chart) svg > path):
(.timeline-view.cpu .gauge-chart:not(.empty) > svg > path.low):
(.timeline-view.cpu .gauge-chart:not(.empty) > svg > path.medium):
(.timeline-view.cpu .gauge-chart:not(.empty) > svg > path.high):
(.timeline-view.cpu .gauge-chart:not(.empty) > svg > polygon.needle):
(.timeline-view.cpu .energy):
(.timeline-view.cpu .energy .energy-impact):
(.timeline-view.cpu .energy .energy-impact.low):
(.timeline-view.cpu .energy .energy-impact.medium):
(.timeline-view.cpu .energy .energy-impact.high):
(.timeline-view.cpu .energy .energy-impact-number):
Styling the chart and text for the different energy impact levels.

* UserInterface/Views/CPUTimelineView.js:
(WI.CPUTimelineView.prototype.get lowEnergyValue):
(WI.CPUTimelineView.prototype.get highEnergyValue):
(WI.CPUTimelineView.prototype.initialLayout):
(WI.CPUTimelineView.prototype.layout):
(WI.CPUTimelineView.prototype._layoutEnergyChart.mapWithBias):
(WI.CPUTimelineView.prototype._layoutEnergyChart.valuesForGauge):
(WI.CPUTimelineView.prototype._layoutEnergyChart):
(WI.CPUTimelineView.prototype._clearEnergyImpactText):
New gauge chart and associated popover.
We do a bit of biasing of the data for each of the sections
in the gauge chart. Each section biases toward the cap of the
section so that:
  - we encourage lower power usage (sub 3%)
  - the gauge needle quickly moves past the low value of a range

* UserInterface/Views/GaugeChart.css: Added.
(.gauge-chart):
(body[dir=rtl] .gauge-chart):
(.gauge-chart > svg > path,):
(.gauge-chart > svg > polygon.needle):
(.gauge-chart.empty > svg > polygon.needle):
(@media (prefers-color-scheme: dark)):
* UserInterface/Views/GaugeChart.js: Added.
(WI.GaugeChart.prototype.get size):
(WI.GaugeChart.prototype.get segments):
(WI.GaugeChart.prototype.get value):
(WI.GaugeChart.prototype.set value):
(WI.GaugeChart.prototype.clear):
(WI.GaugeChart.prototype.initialLayout):
(WI.GaugeChart.prototype.layout):
(WI.GaugeChart.prototype._validateSegments):
(WI.GaugeChart.prototype._createSegmentPathData):
GaugeChart with variable number of sections and a
current value needle. It has a bit of customization
when drawing the arc at the start of each segment.

* UserInterface/Views/Variables.css:
(:root):
(@media (prefers-color-scheme: dark)):
New CPU colors for the different energy impact levels.

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

Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
Source/WebInspectorUI/UserInterface/Main.html
Source/WebInspectorUI/UserInterface/Views/CPUTimelineView.css
Source/WebInspectorUI/UserInterface/Views/CPUTimelineView.js
Source/WebInspectorUI/UserInterface/Views/ChartDetailsSectionRow.js
Source/WebInspectorUI/UserInterface/Views/CircleChart.js
Source/WebInspectorUI/UserInterface/Views/GaugeChart.css [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/GaugeChart.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/Variables.css

index d7ea26c..22ec26c 100644 (file)
@@ -1,3 +1,76 @@
+2019-03-01  Joseph Pecoraro  <pecoraro@apple.com>
+
+        Web Inspector: CPU Usage - Energy Impact Section
+        https://bugs.webkit.org/show_bug.cgi?id=195151
+
+        Reviewed by Devin Rousso.
+
+        * Localizations/en.lproj/localizedStrings.js:
+        * UserInterface/Main.html:
+        New strings and resources.
+
+        * UserInterface/Views/CPUTimelineView.css:
+        (.timeline-view.cpu > .content .subtitle > .info):
+        (@media (prefers-color-scheme: dark)):
+        (.energy-info-popover-content):
+        (.timeline-view.cpu > .content > .overview > .divider):
+        (body[dir=ltr] .timeline-view.cpu > .content > .overview > .divider):
+        (body[dir=rtl] .timeline-view.cpu > .content > .overview > .divider):
+        (.timeline-view.cpu :matches(.area-chart, .stacked-area-chart) svg > path):
+        (.timeline-view.cpu .gauge-chart:not(.empty) > svg > path.low):
+        (.timeline-view.cpu .gauge-chart:not(.empty) > svg > path.medium):
+        (.timeline-view.cpu .gauge-chart:not(.empty) > svg > path.high):
+        (.timeline-view.cpu .gauge-chart:not(.empty) > svg > polygon.needle):
+        (.timeline-view.cpu .energy):
+        (.timeline-view.cpu .energy .energy-impact):
+        (.timeline-view.cpu .energy .energy-impact.low):
+        (.timeline-view.cpu .energy .energy-impact.medium):
+        (.timeline-view.cpu .energy .energy-impact.high):
+        (.timeline-view.cpu .energy .energy-impact-number):
+        Styling the chart and text for the different energy impact levels.
+
+        * UserInterface/Views/CPUTimelineView.js:
+        (WI.CPUTimelineView.prototype.get lowEnergyValue):
+        (WI.CPUTimelineView.prototype.get highEnergyValue):
+        (WI.CPUTimelineView.prototype.initialLayout):
+        (WI.CPUTimelineView.prototype.layout):
+        (WI.CPUTimelineView.prototype._layoutEnergyChart.mapWithBias):
+        (WI.CPUTimelineView.prototype._layoutEnergyChart.valuesForGauge):
+        (WI.CPUTimelineView.prototype._layoutEnergyChart):
+        (WI.CPUTimelineView.prototype._clearEnergyImpactText):
+        New gauge chart and associated popover.
+        We do a bit of biasing of the data for each of the sections
+        in the gauge chart. Each section biases toward the cap of the
+        section so that:
+          - we encourage lower power usage (sub 3%)
+          - the gauge needle quickly moves past the low value of a range
+
+        * UserInterface/Views/GaugeChart.css: Added.
+        (.gauge-chart):
+        (body[dir=rtl] .gauge-chart):
+        (.gauge-chart > svg > path,):
+        (.gauge-chart > svg > polygon.needle):
+        (.gauge-chart.empty > svg > polygon.needle):
+        (@media (prefers-color-scheme: dark)):
+        * UserInterface/Views/GaugeChart.js: Added.
+        (WI.GaugeChart.prototype.get size):
+        (WI.GaugeChart.prototype.get segments):
+        (WI.GaugeChart.prototype.get value):
+        (WI.GaugeChart.prototype.set value):
+        (WI.GaugeChart.prototype.clear):
+        (WI.GaugeChart.prototype.initialLayout):
+        (WI.GaugeChart.prototype.layout):
+        (WI.GaugeChart.prototype._validateSegments):
+        (WI.GaugeChart.prototype._createSegmentPathData):
+        GaugeChart with variable number of sections and a
+        current value needle. It has a bit of customization
+        when drawing the arc at the start of each segment.
+
+        * UserInterface/Views/Variables.css:
+        (:root):
+        (@media (prefers-color-scheme: dark)):
+        New CPU colors for the different energy impact levels.
+
 2019-03-01  Nikita Vasilyev  <nvasilyev@apple.com>
 
         Web Inspector: Data grid border colors don't match accent colors
index 1fbd961..479c0eb 100644 (file)
@@ -141,6 +141,7 @@ localizedStrings["Author Stylesheet"] = "Author Stylesheet";
 localizedStrings["Auto Increment"] = "Auto Increment";
 localizedStrings["Automatically continue after evaluating"] = "Automatically continue after evaluating";
 localizedStrings["Available Style Sheets"] = "Available Style Sheets";
+localizedStrings["Average CPU: %s"] = "Average CPU: %s";
 localizedStrings["Average Time"] = "Average Time";
 localizedStrings["Average: %s"] = "Average: %s";
 localizedStrings["Back (%s)"] = "Back (%s)";
@@ -338,6 +339,7 @@ localizedStrings["Download"] = "Download";
 localizedStrings["Download Web Archive"] = "Download Web Archive";
 localizedStrings["Duplicate property"] = "Duplicate property";
 localizedStrings["Duration"] = "Duration";
+localizedStrings["Duration: %s"] = "Duration: %s";
 localizedStrings["Dynamically calculated for the parent element"] = "Dynamically calculated for the parent element";
 localizedStrings["Dynamically calculated for the selected element"] = "Dynamically calculated for the selected element";
 localizedStrings["Dynamically calculated for the selected element and did not match"] = "Dynamically calculated for the selected element and did not match";
@@ -393,6 +395,7 @@ localizedStrings["Enable source maps"] = "Enable source maps";
 localizedStrings["Enabled"] = "Enabled";
 localizedStrings["Encoded"] = "Encoded";
 localizedStrings["Encoding"] = "Encoding";
+localizedStrings["Energy Impact"] = "Energy Impact";
 localizedStrings["Ensure aria-hidden=\u0022%s\u0022 is not used."] = "Ensure aria-hidden=\u0022%s\u0022 is not used.";
 localizedStrings["Ensure that \u0022%s\u0022 is spelled correctly."] = "Ensure that \u0022%s\u0022 is spelled correctly.";
 localizedStrings["Ensure that buttons have accessible labels for assistive technology."] = "Ensure that buttons have accessible labels for assistive technology.";
@@ -413,6 +416,7 @@ localizedStrings["Error"] = "Error";
 localizedStrings["Error: "] = "Error: ";
 localizedStrings["Errors"] = "Errors";
 localizedStrings["Errors:"] = "Errors:";
+localizedStrings["Estimated energy impact."] = "Estimated energy impact.";
 localizedStrings["Eval Code"] = "Eval Code";
 localizedStrings["Evaluate JavaScript"] = "Evaluate JavaScript";
 localizedStrings["Event"] = "Event";
@@ -713,6 +717,7 @@ localizedStrings["Pause Reason"] = "Pause Reason";
 localizedStrings["Pause script execution (%s or %s)"] = "Pause script execution (%s or %s)";
 /* The number of tests that passed expressed as a percentage, followed by a literal %. */
 localizedStrings["Percentage (of audits)"] = "%s%%";
+localizedStrings["Periods of high CPU utilization will rapidly drain battery. Strive to keep idle pages under %s average CPU utilization."] = "Periods of high CPU utilization will rapidly drain battery. Strive to keep idle pages under %s average CPU utilization.";
 localizedStrings["Ping"] = "Ping";
 localizedStrings["Ping Frame"] = "Ping Frame";
 localizedStrings["Pings"] = "Pings";
@@ -883,6 +888,7 @@ localizedStrings["Shader Programs"] = "Shader Programs";
 localizedStrings["Shadow Content"] = "Shadow Content";
 localizedStrings["Shadow Content (%s)"] = "Shadow Content (%s)";
 localizedStrings["Shared Focus"] = "Shared Focus";
+localizedStrings["Short"] = "Short";
 localizedStrings["Shortest property path to %s"] = "Shortest property path to %s";
 localizedStrings["Show %d More"] = "Show %d More";
 localizedStrings["Show All"] = "Show All";
@@ -988,6 +994,7 @@ localizedStrings["The \u201C%s\u201D audit threw an error"] = "The \u201C%s\u201
 localizedStrings["The \u201C%s\u201D\ntable is empty."] = "The \u201C%s\u201D\ntable is empty.";
 localizedStrings["The page's content has changed"] = "The page's content has changed";
 localizedStrings["The resource was requested insecurely."] = "The resource was requested insecurely.";
+localizedStrings["There is an incurred energy penalty each time the page enters script. This commonly happens with timers, event handlers, and observers."] = "There is an incurred energy penalty each time the page enters script. This commonly happens with timers, event handlers, and observers.";
 localizedStrings["These are all of the different test result levels."] = "These are all of the different test result levels.";
 localizedStrings["These are all of the different types of data that can be returned with the test result."] = "These are all of the different types of data that can be returned with the test result.";
 localizedStrings["These tests serve as a demonstration of the functionality and structure of audits."] = "These tests serve as a demonstration of the functionality and structure of audits.";
@@ -1020,6 +1027,7 @@ localizedStrings["Timer Installed"] = "Timer Installed";
 localizedStrings["Timer Removed"] = "Timer Removed";
 localizedStrings["Timestamp \u2014 %s"] = "Timestamp \u2014 %s";
 localizedStrings["Timing"] = "Timing";
+localizedStrings["To improve CPU utilization reduce or batch workloads when the page is not visible or during times when the page is not being interacted with."] = "To improve CPU utilization reduce or batch workloads when the page is not visible or during times when the page is not being interacted with.";
 localizedStrings["Toggle Classes"] = "Toggle Classes";
 localizedStrings["Toggle Visibility"] = "Toggle Visibility";
 localizedStrings["Top Functions"] = "Top Functions";
@@ -1069,6 +1077,7 @@ localizedStrings["Verbose"] = "Verbose";
 localizedStrings["Version"] = "Version";
 localizedStrings["Vertex"] = "Vertex";
 localizedStrings["Vertex Shader"] = "Vertex Shader";
+localizedStrings["Very High"] = "Very High";
 localizedStrings["View Image"] = "View Image";
 localizedStrings["View Recording"] = "View Recording";
 localizedStrings["View Shader"] = "View Shader";
index f8bb5fd..b7ab243 100644 (file)
     <link rel="stylesheet" href="Views/FolderIcon.css">
     <link rel="stylesheet" href="Views/FontResourceContentView.css">
     <link rel="stylesheet" href="Views/FormattedValue.css">
+    <link rel="stylesheet" href="Views/GaugeChart.css">
     <link rel="stylesheet" href="Views/GeneralStyleDetailsSidebarPanel.css">
     <link rel="stylesheet" href="Views/GoToLineDialog.css">
     <link rel="stylesheet" href="Views/GradientEditor.css">
     <script src="Views/FormattedValue.js"></script>
     <script src="Views/FrameDOMTreeContentView.js"></script>
     <script src="Views/FrameTreeElement.js"></script>
+    <script src="Views/GaugeChart.js"></script>
     <script src="Views/GeneralTreeElementPathComponent.js"></script>
     <script src="Views/GenericResourceContentView.js"></script>
     <script src="Views/GoToLineDialog.js"></script>
index 168a0ea..2bbf483 100644 (file)
@@ -40,6 +40,23 @@ body .timeline-view.cpu {
     font-size: 14px;
 }
 
+.timeline-view.cpu > .content .subtitle > .info {
+    display: inline-block;
+    width: 15px;
+    height: 15px;
+    -webkit-margin-start: 7px;
+    color: white;
+    font-size: 12px;
+    background-color: darkgray;
+    border-radius: 50%;
+}
+
+.energy-info-popover-content {
+    width: 275px;
+    padding: 0 5px;
+    -webkit-hyphens: auto;
+}
+
 .timeline-view.cpu > .content > .details {
     position: relative;
 }
@@ -89,6 +106,20 @@ body[dir=rtl] .timeline-view.cpu > .content > .details > .timeline-ruler {
     justify-content: center;
 }
 
+.timeline-view.cpu > .content > .overview > .divider {
+    margin: 0 5px;
+
+    --cpu-timeline-view-overview-divider-border-end: 1px solid var(--border-color);
+}
+
+body[dir=ltr] .timeline-view.cpu > .content > .overview > .divider {
+    border-right: var(--cpu-timeline-view-overview-divider-border-end);
+}
+
+body[dir=rtl] .timeline-view.cpu > .content > .overview > .divider {
+    border-left: var(--cpu-timeline-view-overview-divider-border-end);
+}
+
 .timeline-view.cpu > .content > .overview .samples,
 .timeline-view.cpu > .content > .overview .legend .size {
     margin: auto;
@@ -165,7 +196,7 @@ body[dir=rtl] .timeline-view.cpu > .content > .details > .timeline-ruler {
     fill: var(--cpu-paint-fill-color);
 }
 
-.timeline-view.cpu svg > path {
+.timeline-view.cpu :matches(.area-chart, .stacked-area-chart) svg > path {
     stroke: var(--cpu-stroke-color);
     fill: var(--cpu-fill-color);
 }
@@ -239,3 +270,61 @@ body[dir=rtl] .timeline-view.cpu :matches(.area-chart, .stacked-area-chart) .mar
     stroke: var(--cpu-paint-stroke-color);
     fill: var(--cpu-paint-fill-color);
 }
+
+.timeline-view.cpu .gauge-chart .low {
+    --gauge-chart-path-fill-color: var(--cpu-low-color);
+    --gauge-chart-path-stroke-color: var(--cpu-low-color);
+}
+
+.timeline-view.cpu .gauge-chart .medium {
+    --gauge-chart-path-fill-color: var(--cpu-medium-color);
+    --gauge-chart-path-stroke-color: var(--cpu-medium-color);
+}
+
+.timeline-view.cpu .gauge-chart .high {
+    --gauge-chart-path-fill-color: var(--cpu-high-color);
+    --gauge-chart-path-stroke-color: var(--cpu-high-color);
+}
+
+.timeline-view.cpu .gauge-chart {
+    --gauge-chart-needle-fill-color: hsla(0, 0%, 36%, 0.85);
+    --gauge-chart-needle-stroke-color: hsla(0, 0%, 36%, 0.85);
+}
+
+.timeline-view.cpu .energy {
+    color: hsla(0, 0%, var(--foreground-lightness), 0.85);
+}
+
+.timeline-view.cpu .energy .energy-impact {
+    min-width: 140px;
+    margin-top: 15px;
+    font-size: 3em;
+    color: var(--text-color-secondary);
+}
+
+.timeline-view.cpu .energy .energy-impact.low {
+    color: var(--cpu-low-color);
+}
+
+.timeline-view.cpu .energy .energy-impact.medium {
+    color: var(--cpu-medium-color);
+}
+
+.timeline-view.cpu .energy .energy-impact.high {
+    color: var(--cpu-high-color);
+}
+
+.timeline-view.cpu .energy .energy-impact-number {
+    font-size: 14px;
+}
+
+@media (prefers-color-scheme: dark) {
+    .timeline-view.cpu > .content .subtitle > .info {
+        background-color: gray;
+    }
+
+    .timeline-view.cpu .gauge-chart:not(.empty) > svg > polygon.needle {
+        fill: hsla(0, 0%, var(--foreground-lightness), 0.85);
+        stroke: hsla(0, 0%, var(--foreground-lightness), 0.85);
+    }
+}
index 1a1c4ee..e3b1fd5 100644 (file)
@@ -59,6 +59,10 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
     static get threadCPUUsageViewHeight() { return 65; }
     static get indicatorViewHeight() { return 15; }
 
+    static get lowEnergyThreshold() { return 3; }
+    static get mediumEnergyThreshold() { return 50; }
+    static get highEnergyThreshold() { return 150; }
+
     // Public
 
     shown()
@@ -91,6 +95,10 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
         this._breakdownChart.needsLayout();
         this._clearBreakdownLegend();
 
+        this._energyChart.clear();
+        this._energyChart.needsLayout();
+        this._clearEnergyImpactText();
+
         function clearUsageView(view) {
             view.clear();
 
@@ -178,6 +186,75 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
         this._breakdownLegendPaintElement = appendLegendRow(this._breakdownLegendElement, WI.CPUTimelineView.SampleType.Paint);
         this._breakdownLegendStyleElement = appendLegendRow(this._breakdownLegendElement, WI.CPUTimelineView.SampleType.Style);
 
+        let dividerElement = overviewElement.appendChild(document.createElement("div"));
+        dividerElement.classList.add("divider");
+
+        let energyTooltip = WI.UIString("Estimated energy impact.")
+        let energyContainerElement = createChartContainer(overviewElement, WI.UIString("Energy Impact"), energyTooltip);
+        energyContainerElement.classList.add("energy");
+
+        let energyChartElement = energyContainerElement.parentElement;
+        let energySubtitleElement = energyChartElement.firstChild;
+        let energyInfoElement = energySubtitleElement.appendChild(document.createElement("span"));
+        energyInfoElement.className = "info";
+        energyInfoElement.textContent = "?";
+
+        this._energyInfoPopover = null;
+        this._energyInfoPopoverContentElement = null;
+        energyInfoElement.addEventListener("click", (event) => {
+            if (!this._energyInfoPopover)
+                this._energyInfoPopover = new WI.Popover;
+
+            if (!this._energyInfoPopoverContentElement) {
+                this._energyInfoPopoverContentElement = document.createElement("div");
+                this._energyInfoPopoverContentElement.className = "energy-info-popover-content";
+
+                const precision = 0;
+                let lowPercent = Number.percentageString(CPUTimelineView.lowEnergyThreshold / 100, precision);
+
+                let p1 = this._energyInfoPopoverContentElement.appendChild(document.createElement("p"));
+                p1.textContent = WI.UIString("Periods of high CPU utilization will rapidly drain battery. Strive to keep idle pages under %s average CPU utilization.").format(lowPercent);
+
+                let p2 = this._energyInfoPopoverContentElement.appendChild(document.createElement("p"));
+                p2.textContent = WI.UIString("There is an incurred energy penalty each time the page enters script. This commonly happens with timers, event handlers, and observers.");
+
+                let p3 = this._energyInfoPopoverContentElement.appendChild(document.createElement("p"));
+                p3.textContent = WI.UIString("To improve CPU utilization reduce or batch workloads when the page is not visible or during times when the page is not being interacted with.");
+            }
+
+            let isRTL = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL;
+            let preferredEdges = isRTL ? [WI.RectEdge.MAX_Y, WI.RectEdge.MIN_X] : [WI.RectEdge.MAX_Y, WI.RectEdge.MAX_X];
+            let calculateTargetFrame = () => WI.Rect.rectFromClientRect(energyInfoElement.getBoundingClientRect()).pad(3);
+
+            this._energyInfoPopover.presentNewContentWithFrame(this._energyInfoPopoverContentElement, calculateTargetFrame(), preferredEdges);
+            this._energyInfoPopover.windowResizeHandler = () => {
+                this._energyInfoPopover.present(calculateTargetFrame(), preferredEdges);
+            };
+        });
+
+        this._energyChart = new WI.GaugeChart({
+            height: 110,
+            strokeWidth: 20,
+            segments: [
+                {className: "low", limit: 10},
+                {className: "medium", limit: 80},
+                {className: "high", limit: 100},
+            ]
+        });
+        this.addSubview(this._energyChart);
+        energyContainerElement.appendChild(this._energyChart.element);
+
+        let energyTextContainerElement = energyContainerElement.appendChild(document.createElement("div"));
+
+        this._energyImpactLabelElement = energyTextContainerElement.appendChild(document.createElement("div"));
+        this._energyImpactLabelElement.className = "energy-impact";
+
+        this._energyImpactNumberElement = energyTextContainerElement.appendChild(document.createElement("div"));
+        this._energyImpactNumberElement.className = "energy-impact-number";
+
+        this._energyImpactDurationElement = energyTextContainerElement.appendChild(document.createElement("div"));
+        this._energyImpactDurationElement.className = "energy-impact-number";
+
         let detailsContainerElement = contentElement.appendChild(document.createElement("div"));
         detailsContainerElement.classList.add("details");
 
@@ -255,6 +332,7 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
         let graphStartTime = this.startTime;
         let graphEndTime = this.endTime;
         let visibleEndTime = Math.min(this.endTime, this.currentTime);
+        let visibleDuration = visibleEndTime - graphStartTime;
 
         let discontinuities = this._recording.discontinuitiesInTimeRange(graphStartTime, visibleEndTime);
         let originalDiscontinuities = discontinuities.slice();
@@ -560,10 +638,64 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
         let graphWidth = (graphEndTime - graphStartTime) / secondsPerPixel;
         let size = new WI.Size(graphWidth, CPUTimelineView.indicatorViewHeight);
         this._mainThreadWorkIndicatorView.updateChart(samplingData.samples, size, visibleEndTime, xScaleIndicatorRange);
+
+        this._layoutEnergyChart(average, visibleDuration);
     }
 
     // Private
 
+    _layoutEnergyChart(average, visibleDuration)
+    {
+        // The lower the bias value [0..1], the more it increases the skew towards rangeHigh.
+        function mapWithBias(value, rangeLow, rangeHigh, outputRangeLow, outputRangeHigh, bias) {
+            console.assert(value >= rangeLow && value <= rangeHigh, "value was not in range.", value);
+            let percentInRange = (value - rangeLow) / (rangeHigh - rangeLow);
+            let skewedPercent = Math.pow(percentInRange, bias);
+            let valueInOutputRange = (skewedPercent * (outputRangeHigh - outputRangeLow)) + outputRangeLow;
+            return valueInOutputRange;
+        }
+
+        this._clearEnergyImpactText();
+
+        if (average === 0) {
+             // Zero. (0% CPU, mapped to 0)
+            this._energyImpactLabelElement.textContent = WI.UIString("Low");
+            this._energyImpactLabelElement.classList.add("low");
+            this._energyChart.value = 0;
+        } else if (average <= CPUTimelineView.lowEnergyThreshold) {
+            // Low. (<=3% CPU, mapped to 0-10)
+            this._energyImpactLabelElement.textContent = WI.UIString("Low");
+            this._energyImpactLabelElement.classList.add("low");
+            this._energyChart.value = mapWithBias(average, 0, CPUTimelineView.lowEnergyThreshold, 0, 10, 0.85);
+        } else if (average <= CPUTimelineView. mediumEnergyThreshold) {
+            // Medium (3%-90% CPU, mapped to 10-80)
+            this._energyImpactLabelElement.textContent = WI.UIString("Medium");
+            this._energyImpactLabelElement.classList.add("medium");
+            this._energyChart.value = mapWithBias(average, CPUTimelineView.lowEnergyThreshold, CPUTimelineView.mediumEnergyThreshold, 10, 80, 0.6);
+        } else if (average < CPUTimelineView. highEnergyThreshold) {
+            // High. (50-150% CPU, mapped to 80-100)
+            this._energyImpactLabelElement.textContent = WI.UIString("High");
+            this._energyImpactLabelElement.classList.add("high");
+            this._energyChart.value = mapWithBias(average, CPUTimelineView.mediumEnergyThreshold, CPUTimelineView.highEnergyThreshold, 80, 100, 0.9);
+        } else {
+            // Very High. (>150% CPU, mapped to 100)
+            this._energyImpactLabelElement.textContent = WI.UIString("Very High");
+            this._energyImpactLabelElement.classList.add("high");
+            this._energyChart.value = 100;
+        }
+
+        this._energyChart.needsLayout();
+
+        this._energyImpactNumberElement.textContent = WI.UIString("Average CPU: %s").format(Number.percentageString(average / 100));
+
+        if (visibleDuration < 5)
+            this._energyImpactDurationElement.textContent = WI.UIString("Duration: %s").format(WI.UIString("Short"));
+        else {
+            let durationDisplayString = Math.floor(visibleDuration) + "s";
+            this._energyImpactDurationElement.textContent = WI.UIString("Duration: %s").format(durationDisplayString);
+        }
+    }
+
     _computeSamplingData(startTime, endTime)
     {
         // Compute per-millisecond samples of what the main thread was doing.
@@ -726,6 +858,14 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
         this._workerViews = [];
     }
 
+    _clearEnergyImpactText()
+    {
+        this._energyImpactLabelElement.classList.remove("low", "medium", "high");
+        this._energyImpactLabelElement.textContent = emDash;
+        this._energyImpactNumberElement.textContent = "";
+        this._energyImpactDurationElement.textContent = "";
+    }
+
     _clearBreakdownLegend()
     {
         this._breakdownLegendScriptElement.textContent = emDash;
index 987daaa..8f3f054 100644 (file)
@@ -312,7 +312,7 @@ WI.ChartDetailsSectionRow = class ChartDetailsSectionRow extends WI.DetailsSecti
             return [
                 "M", x1, y1,                                // Starting position.
                 "A", r1, r1, 0, largeArcFlag, 1, x2, y2,    // Draw outer arc.
-                "L", x3, y3,                                // Connect outer and innner arcs.
+                "L", x3, y3,                                // Connect outer and inner arcs.
                 "A", r2, r2, 0, largeArcFlag, 0, x4, y4,    // Draw inner arc.
                 "Z"                                         // Close path.
             ].join(" ");
index 91c9ffc..481cefa 100644 (file)
@@ -201,7 +201,7 @@ WI.CircleChart = class CircleChart extends WI.View
         return [
             "M", x1, y1,                                // Starting position.
             "A", r1, r1, 0, largeArcFlag, 1, x2, y2,    // Draw outer arc.
-            "L", x3, y3,                                // Connect outer and innner arcs.
+            "L", x3, y3,                                // Connect outer and inner arcs.
             "A", r2, r2, 0, largeArcFlag, 0, x4, y4,    // Draw inner arc.
             "Z"                                         // Close path.
         ].join(" ");
diff --git a/Source/WebInspectorUI/UserInterface/Views/GaugeChart.css b/Source/WebInspectorUI/UserInterface/Views/GaugeChart.css
new file mode 100644 (file)
index 0000000..ad65e58
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+.gauge-chart {
+    position: relative;
+}
+
+body[dir=rtl] .gauge-chart {
+    transform: scaleX(-1);
+}
+
+.gauge-chart > svg > path,
+.gauge-chart > svg > polygon {
+    stroke-width: 1;
+    transition-property: transform, fill, stroke;
+    transition-duration: 0.2s;
+}
+
+.gauge-chart:not(.empty) > svg > path {
+    fill: var(--gauge-chart-path-fill-color, hsla(0, 0%, 0%, 0.02));
+    stroke: var(--gauge-chart-path-stroke-color, hsla(0, 0%, var(--foreground-lightness), 0.1));
+}
+
+.gauge-chart:not(.empty) > svg > .needle {
+    fill: var(--gauge-chart-needle-fill-color, gray);
+    stroke: var(--gauge-chart-needle-stroke-color, gray);
+}
+
+.gauge-chart.empty > svg > path {
+    fill: hsla(0, 0%, 0%, 0.02);
+    stroke: hsla(0, 0%, var(--foreground-lightness), 0.1);
+}
+
+.gauge-chart.empty > svg > .needle {
+    fill: hsl(0, 0%, 88%);
+    stroke: hsla(0, 0%, var(--foreground-lightness), 0.1);
+}
+
+@media (prefers-color-scheme: dark) {
+    .gauge-chart.empty > svg > .needle {
+        fill: hsl(0, 0%, 20%);
+    }
+}
diff --git a/Source/WebInspectorUI/UserInterface/Views/GaugeChart.js b/Source/WebInspectorUI/UserInterface/Views/GaugeChart.js
new file mode 100644 (file)
index 0000000..07902e1
--- /dev/null
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+// GaugeChart creates a semi-circle gauge chart with colored segments.
+//
+// Initialize the chart with a semi-circle height, stroke width, and segments.
+// The class names you provide for the segments will allow you to style them
+// and the limit (0 - 100) is the upper percentage value where that segment
+// ends. You can update the chart with new current needle value at any time.
+//
+// SVG:
+//
+// - There is a path for each segment. Note there is a small includes a
+//   buffer between segments, so they should be more than a few # apart.
+// - There is a single polygon for the needle value.
+//
+//  <div class="gauge-chart">
+//      <svg width="204" height="110" viewBox="0 0 204 110">
+//          <path class="segment segment-class-name-1" d="..."/>
+//          <path class="segment segment-class-name-2" d="..."/>
+//          ...
+//          <polygon class="needle" points="..."/>
+//      </svg>
+//  </div>
+
+WI.GaugeChart = class GaugeChart extends WI.View
+{
+    constructor({height, strokeWidth, segments})
+    {
+        super();
+
+        strokeWidth = strokeWidth || 10;
+
+        this._needleValue = null;
+
+        const needleOverhangSpace = 10; // Distance the needle goes past the outer circle edge.
+        const needleUnderhangSpace = 8; // Space allowed beneath the graph so a horizontal needle lines up.
+
+        this._center = height - needleUnderhangSpace;
+        this._radius = height - needleUnderhangSpace - needleOverhangSpace - 1;
+        this._innerRadius = Math.floor(this._radius - strokeWidth);
+
+        let width = (this._radius + needleOverhangSpace + 1) * 2;
+        this._size = new WI.Size(width, height);
+
+        console.assert(!this._segments, "Set segments only once");
+        console.assert(segments.length >= 1, "Need at least one segment");
+        console.assert(this._validateSegments(segments));
+
+        this._segments = segments;
+
+        this.element.classList.add("gauge-chart");
+
+        this._chartElement = this.element.appendChild(createSVGElement("svg"));
+        this._chartElement.setAttribute("width", width);
+        this._chartElement.setAttribute("height", height);
+        this._chartElement.setAttribute("viewBox", `0 0 ${width} ${height}`);
+
+        this._needleElement = null;
+    }
+
+    // Public
+
+    get size() { return this._size; }
+    get segments() { return this._segments; }
+
+    get value()
+    {
+        return this._needleValue;
+    }
+
+    set value(value)
+    {
+        console.assert(value >= 0 && value <= 100, "value should be between 0 and 100.", value);
+
+        this._needleValue = value;
+    }
+
+    clear()
+    {
+        this._needleValue = null;
+    }
+
+    // Protected
+
+    initialLayout()
+    {
+        super.initialLayout();
+
+        let startAngle = Math.PI;
+
+        const onePercentAngle = Math.PI / 100;
+
+        for (let {className, limit} of this._segments) {
+            let offset = limit === 100 ? 0 : 1;
+            let endAngle = Math.PI + (((limit - offset) / 100) * Math.PI);
+
+            let pathElement = this._chartElement.appendChild(createSVGElement("path"));
+            pathElement.classList.add("segment", className);
+            pathElement.setAttribute("d", this._createSegmentPathData(this._center, startAngle, endAngle, this._radius, this._innerRadius));
+
+            startAngle = endAngle + onePercentAngle;
+        }
+
+        const needlePointExtraDraw = 0.5; // Draw a fat tip to the needle.
+        const needleBaseExtraDraw = 4.5; // Draw a fat base to the needle.
+        const needleUnderhangDraw = 6; // Draw the needle underhanging the base of the graph.
+
+        let midX = this.size.width / 2;
+        let midY = this._center;
+
+        this._needleElement = this._chartElement.appendChild(createSVGElement("polygon"));
+        this._needleElement.classList.add("needle");
+        this._needleElement.setAttribute("points", `0,${midY + needlePointExtraDraw}, 0,${midY - needlePointExtraDraw} ${midX + needleUnderhangDraw},${midY - needleBaseExtraDraw} ${midX + needleUnderhangDraw},${midY + needleBaseExtraDraw}`);
+        this._needleElement.style.transformOrigin = `${midX}px ${midY}px`;
+    }
+
+    layout()
+    {
+        super.layout();
+
+        if (this.layoutReason === WI.View.LayoutReason.Resize)
+            return;
+
+        let empty = this._needleValue === null;
+        this.element.classList.toggle("empty", empty);
+
+        let value = empty ? 0 : this._needleValue;
+        let degrees = 180 * (value / 100); // 0-100% mapped to 0-180deg.
+        this._needleElement.style.transform = `rotate(${degrees}deg)`;
+    }
+
+    // Private
+
+    _validateSegments(segments)
+    {
+        let lastLimit = -1;
+
+        for (let {className, limit} of segments) {
+            console.assert(limit >= 1 && limit <= 100, "limit should be between 1 and 100", limit);
+            console.assert(limit >= (lastLimit + 1), "limits should always increase between segments");
+            lastLimit = limit;
+        }
+
+        return true;
+    }
+
+    _createSegmentPathData(c, a1, a2, r1, r2)
+    {
+        const startIndicatorUnderhang = 7;
+        let r3 = (r2 - startIndicatorUnderhang);
+        let onePercentArc = Math.PI / 100;
+        let largeArcFlag = ((a2 - a1) % (Math.PI * 2)) > Math.PI ? 1 : 0;
+
+        let x1 = c + Math.cos(a1) * r1,
+            y1 = c + Math.sin(a1) * r1,
+            x2 = c + Math.cos(a2) * r1,
+            y2 = c + Math.sin(a2) * r1,
+            x3 = c + Math.cos(a2) * r2,
+            y3 = c + Math.sin(a2) * r2,
+            x4 = c + Math.cos(a1 + onePercentArc) * r2,
+            y4 = c + Math.sin(a1 + onePercentArc) * r2,
+            x5 = c + Math.cos(a1 + onePercentArc) * r3,
+            y5 = c + Math.sin(a1 + onePercentArc) * r3,
+            x6 = c + Math.cos(a1) * r3,
+            y6 = c + Math.sin(a1) * r3;
+
+        return [
+            "M", x1, y1,                                // Starting position.
+            "A", r1, r1, 0, largeArcFlag, 1, x2, y2,    // Draw outer arc.
+            "L", x3, y3,                                // Connect outer and inner arcs.
+            "A", r2, r2, 0, largeArcFlag, 0, x4, y4,    // Draw inner arc.
+            "L", x5, y5,                                // Extend inner arc to center for start indicator.
+            "A", r3, r3, 0, largeArcFlag, 0, x6, y6,    // Draw final inner arc for start indicator.
+            "Z"                                         // Close path.
+        ].join(" ");
+    }
+};
index 0cbd4ab..c592a45 100644 (file)
     --cpu-paint-fill-color: hsl(76, 49%, 75%);
     --cpu-paint-stroke-color: hsl(79, 45%, 50%);
 
+    --cpu-low-color: hsl(110, 52%, 56%);
+    --cpu-medium-color: hsl(46, 91%, 62%);
+    --cpu-high-color: hsl(6, 79%, 57%);
+
     --network-header-color: hsl(204, 52%, 55%);
     --network-system-color: hsl(79, 32%, 50%);
     --network-pseudo-header-color: hsl(312, 35%, 51%);
@@ -284,6 +288,10 @@ body.window-inactive * {
         --network-pseudo-header-color: hsl(312, 55%, 61%);
         --network-error-color: hsl(0, 54%, 55%);
 
+        --cpu-low-color: hsl(110, 52%, 56%);
+        --cpu-medium-color: hsl(46, 91%, 62%);
+        --cpu-high-color: hsl(6, 79%, 57%);
+
         --even-zebra-stripe-row-background-color: var(--background-color);
         --odd-zebra-stripe-row-background-color: var(--background-color-secondary);
         --transparent-stripe-background-gradient: linear-gradient(to bottom, transparent, transparent 50%, hsla(0, 0%, 100%, 0.03) 50%, hsla(0, 0%, 100%, 0.03)) top left / 100% 40px;