Web Inspector: CPU Usage Timeline - Main Thread Indicator
authorjoepeck@webkit.org <joepeck@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 26 Feb 2019 21:40:50 +0000 (21:40 +0000)
committerjoepeck@webkit.org <joepeck@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 26 Feb 2019 21:40:50 +0000 (21:40 +0000)
https://bugs.webkit.org/show_bug.cgi?id=194972

Reviewed by Devin Rousso.

* UserInterface/Main.html:
* UserInterface/Base/Utilities.js:
(value):
The existing enclosingNode doesn't work for SVG because its names
are lowercase. Add a simplified version for the svg case.

* UserInterface/Views/RangeChart.js: Added.
(WI.RangeChart):
(WI.RangeChart.prototype.get size):
(WI.RangeChart.prototype.set size):
(WI.RangeChart.prototype.addRange):
(WI.RangeChart.prototype.clear):
(WI.RangeChart.prototype.layout):
A new chart that draws rects for given ranges.

* UserInterface/Models/Timeline.js:
(WI.Timeline.prototype.recordsOverlappingTimeRange):
Helper to specifically get records touching a range. Useful
for when we have a single pixel spanning (startTime -> endTime)
and we want to find records in that pixel.

* UserInterface/Views/CPUTimelineView.css:
(.timeline-view.cpu .cpu-usage-indicator-view > .graph > .range-chart rect):
(.timeline-view.cpu .cpu-usage-indicator-view > .graph > .range-chart .sample-type-script):
(.timeline-view.cpu .cpu-usage-indicator-view > .graph > .range-chart .sample-type-style):
(.timeline-view.cpu .cpu-usage-indicator-view > .graph > .range-chart .sample-type-layout):
(.timeline-view.cpu .cpu-usage-indicator-view > .graph > .range-chart .sample-type-paint):
* UserInterface/Views/CPUTimelineView.js:
(WI.CPUTimelineView.prototype.get indicatorViewHeight):
(WI.CPUTimelineView.prototype.clear):
(WI.CPUTimelineView.prototype.get scrollableElements):
(WI.CPUTimelineView.prototype.initialLayout):
(WI.CPUTimelineView.prototype.layout):
(WI.CPUTimelineView.prototype._graphPositionForMouseEvent):
(WI.CPUTimelineView.prototype._handleIndicatorClick):
(WI.CPUTimelineView.prototype._attemptSelectIndicatatorTimelineRecord):
(WI.CPUTimelineView.prototype._selectTimelineRecord):
Place the Main Thread Indicator view beneath the big graph.
Clicking inside it selects records in the Timeline Overview.

* UserInterface/Views/CPUUsageIndicatorView.css: Added.
(.cpu-usage-indicator-view):
(.cpu-usage-indicator-view > .details):
(body[dir=ltr] .cpu-usage-indicator-view > .details):
(body[dir=rtl] .cpu-usage-indicator-view > .details):
(body[dir=rtl] .cpu-usage-indicator-view > .graph):
(.cpu-usage-indicator-view > .graph):
(.cpu-usage-indicator-view > .graph,):
* UserInterface/Views/CPUUsageIndicatorView.js: Added.
(WI.CPUUsageIndicatorView):
(WI.CPUUsageIndicatorView.prototype.get chart):
(WI.CPUUsageIndicatorView.prototype.clear):
(WI.CPUUsageIndicatorView.prototype.updateChart):
Converts the CPU samples data into a RangeChart. It works to coalesce
many samples of the same type into a single range to reduce total ranges.

* UserInterface/Views/TimelineRecordingContentView.js:
(WI.TimelineRecordingContentView):
(WI.TimelineRecordingContentView.prototype._recordSelected):
(WI.TimelineRecordingContentView.prototype._recordWasSelected):
(WI.TimelineRecordingContentView.prototype._selectRecordInTimelineOverview):
(WI.TimelineRecordingContentView.prototype._selectRecordInTimelineView):
* UserInterface/Views/TimelineView.js:
Add a path for a TimelineView to dispatch a record selected event and cause
have the TimelineRecordingContentView react to it by updating the timeline
overview and relevent timeline view.

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

Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/UserInterface/Base/Utilities.js
Source/WebInspectorUI/UserInterface/Main.html
Source/WebInspectorUI/UserInterface/Models/Timeline.js
Source/WebInspectorUI/UserInterface/Views/CPUTimelineView.css
Source/WebInspectorUI/UserInterface/Views/CPUTimelineView.js
Source/WebInspectorUI/UserInterface/Views/CPUUsageIndicatorView.css [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/CPUUsageIndicatorView.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/RangeChart.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/TimelineRecordingContentView.js
Source/WebInspectorUI/UserInterface/Views/TimelineView.js

index 4e4a103..e13aa77 100644 (file)
@@ -1,3 +1,77 @@
+2019-02-26  Joseph Pecoraro  <pecoraro@apple.com>
+
+        Web Inspector: CPU Usage Timeline - Main Thread Indicator
+        https://bugs.webkit.org/show_bug.cgi?id=194972
+
+        Reviewed by Devin Rousso.
+
+        * UserInterface/Main.html:
+        * UserInterface/Base/Utilities.js:
+        (value):
+        The existing enclosingNode doesn't work for SVG because its names
+        are lowercase. Add a simplified version for the svg case.
+
+        * UserInterface/Views/RangeChart.js: Added.
+        (WI.RangeChart):
+        (WI.RangeChart.prototype.get size):
+        (WI.RangeChart.prototype.set size):
+        (WI.RangeChart.prototype.addRange):
+        (WI.RangeChart.prototype.clear):
+        (WI.RangeChart.prototype.layout):
+        A new chart that draws rects for given ranges.
+
+        * UserInterface/Models/Timeline.js:
+        (WI.Timeline.prototype.recordsOverlappingTimeRange):
+        Helper to specifically get records touching a range. Useful
+        for when we have a single pixel spanning (startTime -> endTime)
+        and we want to find records in that pixel.
+
+        * UserInterface/Views/CPUTimelineView.css:
+        (.timeline-view.cpu .cpu-usage-indicator-view > .graph > .range-chart rect):
+        (.timeline-view.cpu .cpu-usage-indicator-view > .graph > .range-chart .sample-type-script):
+        (.timeline-view.cpu .cpu-usage-indicator-view > .graph > .range-chart .sample-type-style):
+        (.timeline-view.cpu .cpu-usage-indicator-view > .graph > .range-chart .sample-type-layout):
+        (.timeline-view.cpu .cpu-usage-indicator-view > .graph > .range-chart .sample-type-paint):
+        * UserInterface/Views/CPUTimelineView.js:
+        (WI.CPUTimelineView.prototype.get indicatorViewHeight):
+        (WI.CPUTimelineView.prototype.clear):
+        (WI.CPUTimelineView.prototype.get scrollableElements):
+        (WI.CPUTimelineView.prototype.initialLayout):
+        (WI.CPUTimelineView.prototype.layout):
+        (WI.CPUTimelineView.prototype._graphPositionForMouseEvent):
+        (WI.CPUTimelineView.prototype._handleIndicatorClick):
+        (WI.CPUTimelineView.prototype._attemptSelectIndicatatorTimelineRecord):
+        (WI.CPUTimelineView.prototype._selectTimelineRecord):
+        Place the Main Thread Indicator view beneath the big graph.
+        Clicking inside it selects records in the Timeline Overview.
+
+        * UserInterface/Views/CPUUsageIndicatorView.css: Added.
+        (.cpu-usage-indicator-view):
+        (.cpu-usage-indicator-view > .details):
+        (body[dir=ltr] .cpu-usage-indicator-view > .details):
+        (body[dir=rtl] .cpu-usage-indicator-view > .details):
+        (body[dir=rtl] .cpu-usage-indicator-view > .graph):
+        (.cpu-usage-indicator-view > .graph):
+        (.cpu-usage-indicator-view > .graph,):
+        * UserInterface/Views/CPUUsageIndicatorView.js: Added.
+        (WI.CPUUsageIndicatorView):
+        (WI.CPUUsageIndicatorView.prototype.get chart):
+        (WI.CPUUsageIndicatorView.prototype.clear):
+        (WI.CPUUsageIndicatorView.prototype.updateChart):
+        Converts the CPU samples data into a RangeChart. It works to coalesce
+        many samples of the same type into a single range to reduce total ranges.
+
+        * UserInterface/Views/TimelineRecordingContentView.js:
+        (WI.TimelineRecordingContentView):
+        (WI.TimelineRecordingContentView.prototype._recordSelected):
+        (WI.TimelineRecordingContentView.prototype._recordWasSelected):
+        (WI.TimelineRecordingContentView.prototype._selectRecordInTimelineOverview):
+        (WI.TimelineRecordingContentView.prototype._selectRecordInTimelineView):
+        * UserInterface/Views/TimelineView.js:
+        Add a path for a TimelineView to dispatch a record selected event and cause
+        have the TimelineRecordingContentView react to it by updating the timeline
+        overview and relevent timeline view.
+
 2019-02-26  Devin Rousso  <drousso@apple.com>
 
         Web Inspector: navigation sidebar says "No Search Results" when a slow search is in progress
index 0c97ddd..bb3d1db 100644 (file)
@@ -197,11 +197,11 @@ Object.defineProperty(Node.prototype, "enclosingNodeOrSelfWithNodeNameInArray",
 {
     value(nodeNames)
     {
-        let upperCaseNodeNames = nodeNames.map((name) => name.toUpperCase());
+        let lowerCaseNodeNames = nodeNames.map((name) => name.toLowerCase());
 
         for (let node = this; node; node = node.parentElement) {
-            for (let nodeName of upperCaseNodeNames) {
-                if (node.nodeName === nodeName)
+            for (let nodeName of lowerCaseNodeNames) {
+                if (node.nodeName.toLowerCase() === nodeName)
                     return node;
             }
         }
index d3cd636..f8bb5fd 100644 (file)
@@ -45,6 +45,7 @@
     <link rel="stylesheet" href="Views/ButtonToolbarItem.css">
     <link rel="stylesheet" href="Views/CPUTimelineOverviewGraph.css">
     <link rel="stylesheet" href="Views/CPUTimelineView.css">
+    <link rel="stylesheet" href="Views/CPUUsageIndicatorView.css">
     <link rel="stylesheet" href="Views/CPUUsageStackedView.css">
     <link rel="stylesheet" href="Views/CPUUsageView.css">
     <link rel="stylesheet" href="Views/CallFrameIcons.css">
     <script src="Views/ButtonToolbarItem.js"></script>
     <script src="Views/CPUTimelineOverviewGraph.js"></script>
     <script src="Views/CPUTimelineView.js"></script>
+    <script src="Views/CPUUsageIndicatorView.js"></script>
     <script src="Views/CPUUsageStackedView.js"></script>
     <script src="Views/CPUUsageView.js"></script>
     <script src="Views/CSSStyleSheetTreeElement.js"></script>
     <script src="Views/QuickConsole.js"></script>
     <script src="Views/QuickConsoleNavigationBar.js"></script>
     <script src="Views/RadioButtonNavigationItem.js"></script>
+    <script src="Views/RangeChart.js"></script>
     <script src="Views/RecordingActionTreeElement.js"></script>
     <script src="Views/RecordingContentView.js"></script>
     <script src="Views/RecordingStateDetailsSidebarPanel.js"></script>
index 756ce57..82949cc 100644 (file)
@@ -93,6 +93,14 @@ WI.Timeline = class Timeline extends WI.Object
         this.dispatchEventToListeners(WI.Timeline.Event.Refreshed);
     }
 
+    recordsOverlappingTimeRange(startTime, endTime)
+    {
+        let lowerIndex = this._records.lowerBound(startTime, (time, record) => time - record.endTime);
+        let upperIndex = this._records.upperBound(endTime, (time, record) => time - record.startTime);
+
+        return this._records.slice(lowerIndex, upperIndex);
+    }
+
     recordsInTimeRange(startTime, endTime, includeRecordBeforeStart)
     {
         let lowerIndex = this._records.lowerBound(startTime, (time, record) => time - record.startTime);
index f062060..17e40df 100644 (file)
@@ -214,3 +214,27 @@ body[dir=rtl] .timeline-view.cpu :matches(.area-chart, .stacked-area-chart) .mar
     color: var(--text-color-secondary);
     background-color: var(--background-color-content);
 }
+
+.timeline-view.cpu .cpu-usage-indicator-view > .graph > .range-chart rect {
+    stroke-opacity: 0.25;
+}
+
+.timeline-view.cpu .cpu-usage-indicator-view > .graph > .range-chart .sample-type-script {
+    stroke: var(--cpu-script-stroke-color);
+    fill: var(--cpu-script-fill-color);
+}
+
+.timeline-view.cpu .cpu-usage-indicator-view > .graph > .range-chart .sample-type-style {
+    stroke: var(--cpu-style-stroke-color);
+    fill: var(--cpu-style-fill-color);
+}
+
+.timeline-view.cpu .cpu-usage-indicator-view > .graph > .range-chart .sample-type-layout {
+    stroke: var(--cpu-layout-stroke-color);
+    fill: var(--cpu-layout-fill-color);
+}
+
+.timeline-view.cpu .cpu-usage-indicator-view > .graph > .range-chart .sample-type-paint {
+    stroke: var(--cpu-paint-stroke-color);
+    fill: var(--cpu-paint-fill-color);
+}
index 4e6795f..70ebdb7 100644 (file)
@@ -57,6 +57,7 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
 
     static get cpuUsageViewHeight() { return 150; }
     static get threadCPUUsageViewHeight() { return 65; }
+    static get indicatorViewHeight() { return 15; }
 
     // Public
 
@@ -104,21 +105,24 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
         clearUsageView(this._unknownThreadUsageView);
 
         this._removeWorkerThreadViews();
-    }
 
-    get scrollableElements()
-    {
-        return [this.element];
+        this._mainThreadWorkIndicatorView.clear();
     }
 
     // Protected
 
     get showsFilterBar() { return false; }
 
+    get scrollableElements()
+    {
+        return [this.element];
+    }
+
     initialLayout()
     {
         this.element.style.setProperty("--cpu-usage-stacked-view-height", CPUTimelineView.cpuUsageViewHeight + "px");
         this.element.style.setProperty("--cpu-usage-view-height", CPUTimelineView.threadCPUUsageViewHeight + "px");
+        this.element.style.setProperty("--cpu-usage-indicator-view-height", CPUTimelineView.indicatorViewHeight + "px");
 
         let contentElement = this.element.appendChild(document.createElement("div"));
         contentElement.classList.add("content");
@@ -197,6 +201,12 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
         this.addSubview(this._cpuUsageView);
         this._detailsContainerElement.appendChild(this._cpuUsageView.element);
 
+        this._mainThreadWorkIndicatorView = new WI.CPUUsageIndicatorView;
+        this.addSubview(this._mainThreadWorkIndicatorView);
+        this._detailsContainerElement.appendChild(this._mainThreadWorkIndicatorView.element);
+
+        this._mainThreadWorkIndicatorView.chart.element.addEventListener("click", this._handleIndicatorClick.bind(this));
+
         let threadsSubtitleElement = detailsContainerElement.appendChild(document.createElement("div"));
         threadsSubtitleElement.classList.add("subtitle", "threads");
         threadsSubtitleElement.textContent = WI.UIString("Threads");
@@ -515,6 +525,14 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
 
             layoutView(workerView, "usage", CPUTimelineView.threadCPUUsageViewHeight, {dataPoints: workerData.dataPoints, min: workerData.min, max: workerData.max, average: workerData.average});
         }
+
+        function xScaleIndicatorRange(sampleIndex) {
+            return (sampleIndex / 1000) / secondsPerPixel;
+        }
+
+        let graphWidth = (graphEndTime - graphStartTime) / secondsPerPixel;
+        let size = new WI.Size(graphWidth, CPUTimelineView.indicatorViewHeight);
+        this._mainThreadWorkIndicatorView.updateChart(samplingData.samples, size, visibleEndTime, xScaleIndicatorRange);
     }
 
     // Private
@@ -699,6 +717,115 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
         if (cpuTimelineRecord.startTime >= this.startTime && cpuTimelineRecord.endTime <= this.endTime)
             this.needsLayout();
     }
+
+    _graphPositionForMouseEvent(event)
+    {
+        let svgElement = event.target.enclosingNodeOrSelfWithNodeName("svg");
+        if (!svgElement)
+            return NaN;
+
+        let svgRect = svgElement.getBoundingClientRect();
+        let position = event.pageX - svgRect.left;
+
+        if (WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL)
+            return svgRect.width - position;
+        return position;
+    }
+
+    _handleIndicatorClick(event)
+    {
+        let clickPosition = this._graphPositionForMouseEvent(event);
+        if (isNaN(clickPosition))
+            return;
+
+        let secondsPerPixel = this._timelineRuler.secondsPerPixel;
+        let graphClickTime = clickPosition * secondsPerPixel;
+        let graphStartTime = this.startTime;
+
+        let clickStartTime = graphStartTime + graphClickTime;
+        let clickEndTime = clickStartTime + secondsPerPixel;
+
+        // Try at the exact clicked pixel.
+        if (event.target.localName === "rect") {
+            if (this._attemptSelectIndicatatorTimelineRecord(clickStartTime, clickEndTime))
+                return;
+            console.assert(false, "If the user clicked on a rect there should have been a record in this pixel range");
+        }
+
+        // Spiral out 4 pixels each side to try and select a nearby record.
+        for (let i = 1, delta = 0; i <= 4; ++i) {
+            delta += secondsPerPixel;
+            if (this._attemptSelectIndicatatorTimelineRecord(clickStartTime - delta, clickStartTime))
+                return;
+            if (this._attemptSelectIndicatatorTimelineRecord(clickEndTime, clickEndTime + delta))
+                return;
+        }
+    }
+
+    _attemptSelectIndicatatorTimelineRecord(startTime, endTime)
+    {
+        let layoutTimeline = this._recording.timelineForRecordType(WI.TimelineRecord.Type.Layout);
+        let layoutRecords = layoutTimeline ? layoutTimeline.recordsOverlappingTimeRange(startTime, endTime) : [];
+        layoutRecords = layoutRecords.filter((record) => {
+            switch (record.eventType) {
+            case WI.LayoutTimelineRecord.EventType.RecalculateStyles:
+            case WI.LayoutTimelineRecord.EventType.ForcedLayout:
+            case WI.LayoutTimelineRecord.EventType.Layout:
+            case WI.LayoutTimelineRecord.EventType.Paint:
+            case WI.LayoutTimelineRecord.EventType.Composite:
+                return true;
+            case WI.LayoutTimelineRecord.EventType.InvalidateStyles:
+            case WI.LayoutTimelineRecord.EventType.InvalidateLayout:
+                return false;
+            default:
+                console.error("Unhandled LayoutTimelineRecord.EventType", record.eventType);
+                return false;
+            }
+        });
+
+        if (layoutRecords.length) {
+            this._selectTimelineRecord(layoutRecords[0]);
+            return true;
+        }
+
+        let scriptTimeline = this._recording.timelineForRecordType(WI.TimelineRecord.Type.Script);
+        let scriptRecords = scriptTimeline ? scriptTimeline.recordsOverlappingTimeRange(startTime, endTime) : [];
+        scriptRecords = scriptRecords.filter((record) => {
+            switch (record.eventType) {
+            case WI.ScriptTimelineRecord.EventType.ScriptEvaluated:
+            case WI.ScriptTimelineRecord.EventType.APIScriptEvaluated:
+            case WI.ScriptTimelineRecord.EventType.ObserverCallback:
+            case WI.ScriptTimelineRecord.EventType.EventDispatched:
+            case WI.ScriptTimelineRecord.EventType.MicrotaskDispatched:
+            case WI.ScriptTimelineRecord.EventType.TimerFired:
+            case WI.ScriptTimelineRecord.EventType.AnimationFrameFired:
+                return true;
+            case WI.ScriptTimelineRecord.EventType.AnimationFrameRequested:
+            case WI.ScriptTimelineRecord.EventType.AnimationFrameCanceled:
+            case WI.ScriptTimelineRecord.EventType.TimerInstalled:
+            case WI.ScriptTimelineRecord.EventType.TimerRemoved:
+            case WI.ScriptTimelineRecord.EventType.ProbeSampleRecorded:
+            case WI.ScriptTimelineRecord.EventType.ConsoleProfileRecorded:
+            case WI.ScriptTimelineRecord.EventType.GarbageCollected:
+                return false;
+            default:
+                console.error("Unhandled ScriptTimelineRecord.EventType", record.eventType);
+                return false;
+            }
+        });
+
+        if (scriptRecords.length) {
+            this._selectTimelineRecord(scriptRecords[0]);
+            return true;
+        }
+
+        return false;
+    }
+
+    _selectTimelineRecord(record)
+    {
+        this.dispatchEventToListeners(WI.TimelineView.Event.RecordWasSelected, {record});
+    }
 };
 
 // NOTE: UI follows this order.
diff --git a/Source/WebInspectorUI/UserInterface/Views/CPUUsageIndicatorView.css b/Source/WebInspectorUI/UserInterface/Views/CPUUsageIndicatorView.css
new file mode 100644 (file)
index 0000000..e1e78f9
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+
+.cpu-usage-indicator-view {
+    display: flex;
+    width: 100%;
+    height: calc(var(--cpu-usage-indicator-view-height) + 1px); /* 1 for border-bottom */
+    border-bottom: 1px solid var(--border-color);
+}
+
+.cpu-usage-indicator-view > .details {
+    flex-shrink: 0;
+    width: 150px;
+    -webkit-padding-start: 15px;
+    -webkit-border-end: 1px solid var(--border-color);
+}
+
+body[dir=rtl] .cpu-usage-indicator-view > .graph {
+    transform: scaleX(-1);
+}
+
+.cpu-usage-indicator-view > .graph {
+    position: relative;
+    z-index: calc(var(--timeline-marker-z-index) + 1);
+    background-color: var(--background-color-content);
+}
+
+.cpu-usage-indicator-view > .graph,
+.cpu-usage-indicator-view > .graph > .range-chart,
+.cpu-usage-indicator-view > .graph > .range-chart > svg {
+    width: 100%;
+    height: 100%;
+}
diff --git a/Source/WebInspectorUI/UserInterface/Views/CPUUsageIndicatorView.js b/Source/WebInspectorUI/UserInterface/Views/CPUUsageIndicatorView.js
new file mode 100644 (file)
index 0000000..2705bd4
--- /dev/null
@@ -0,0 +1,109 @@
+/*
+ * 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.
+ */
+
+WI.CPUUsageIndicatorView = class CPUUsageIndicatorView extends WI.View
+{
+    constructor(delegate)
+    {
+        super();
+
+        this.element.classList.add("cpu-usage-indicator-view");
+
+        this._detailsElement = this.element.appendChild(document.createElement("div"));
+        this._detailsElement.classList.add("details");
+
+        this._graphElement = this.element.appendChild(document.createElement("div"));
+        this._graphElement.classList.add("graph");
+
+        this._chart = new WI.RangeChart;
+        this.addSubview(this._chart);
+        this._graphElement.appendChild(this._chart.element);
+    }
+
+    // Public
+
+    get chart() { return this._chart; }
+
+    clear()
+    {
+        this._chart.clear();
+        this._chart.needsLayout();
+    }
+
+    updateChart(samples, size, visibleEndTime, xScale)
+    {
+        console.assert(size instanceof WI.Size);
+
+        this._chart.clear();
+        this._chart.size = size;
+        this._chart.needsLayout();
+
+        if (!samples.length)
+            return;
+
+        // Coalesce ranges of samples.
+        let ranges = [];
+        let currentRange = null;
+        let currentSampleType = undefined;
+        for (let i = 0; i < samples.length; ++i) {
+            // Back to idle, close any current chunk.
+            let type = samples[i];
+            if (!type) {
+                if (currentRange) {
+                    ranges.push(currentRange);
+                    currentRange = null;
+                    currentSampleType = undefined;
+                }
+                continue;
+            }
+
+            // Expand existing chunk.
+            if (type === currentSampleType) {
+                currentRange.endIndex = i;
+                continue;
+            }
+
+            // If type changed, close current chunk.
+            if (currentSampleType) {
+                ranges.push(currentRange);
+                currentRange = null;
+                currentSampleType = undefined;
+            }
+
+            // Start a new chunk.
+            console.assert(!currentRange);
+            console.assert(!currentSampleType);
+            currentRange = {type, startIndex: i, endIndex: i};
+            currentSampleType = type;
+        }
+
+        for (let {type, startIndex, endIndex} of ranges) {
+            let startX = xScale(startIndex);
+            let endX = xScale(endIndex + 1);
+            let width = endX - startX;
+            this._chart.addRange(startX, width, type);
+        }
+    }
+};
diff --git a/Source/WebInspectorUI/UserInterface/Views/RangeChart.js b/Source/WebInspectorUI/UserInterface/Views/RangeChart.js
new file mode 100644 (file)
index 0000000..bc87bd3
--- /dev/null
@@ -0,0 +1,110 @@
+/*
+ * 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.
+ */
+
+// RangeChart creates a chart filled with ranges of equal height.
+//
+//     [   |------|------|   |------|                 ]
+//
+// Initialize the chart with a size. You can then include a new range
+// in the chart by providing an (x, w, class) via `addRange`.
+//
+// SVG:
+//
+// - There is a single rect for each range.
+//
+//  <div class="range-chart">
+//      <svg viewBox="0 0 800 75">
+//          <rect width="<w>" height="100%" transform="translateX(<x>)" class="<class>" />
+//          <rect width="<w>" height="100%" transform="translateX(<x>)" class="<class>" />
+//          ...
+//      </svg>
+//  </div>
+
+WI.RangeChart = class RangeChart extends WI.View
+{
+    constructor()
+    {
+        super();
+
+        this.element.classList.add("range-chart");
+
+        this._svgElement = this.element.appendChild(createSVGElement("svg"));
+        this._svgElement.setAttribute("preserveAspectRatio", "none");
+
+        this._ranges = [];
+        this._size = null;
+    }
+
+    // Public
+
+    get size()
+    {
+        return this._size;
+    }
+
+    set size(size)
+    {
+        if (this._size && this._size.equals(size))
+            return;
+
+        this._size = size;
+
+        this._svgElement.setAttribute("viewBox", `0 0 ${size.width} ${size.height}`);
+    }
+
+    addRange(x, width, className)
+    {
+        this._ranges.push({x, width, className});
+    }
+
+    clear()
+    {
+        this._ranges = [];
+    }
+
+    // Protected
+
+    layout()
+    {
+        super.layout();
+
+        if (this.layoutReason === WI.View.LayoutReason.Resize)
+            return;
+
+        if (!this._size)
+            return;
+
+        this._svgElement.removeChildren();
+
+        let h = 0;
+        for (let {x, width, className} of this._ranges) {
+            let rect = this._svgElement.appendChild(createSVGElement("rect"));
+            rect.setAttribute("width", width);
+            rect.setAttribute("height", this.size.height);
+            rect.setAttribute("transform", `translate(${x}, 0)`);
+            rect.classList.add(className);
+        }
+    }
+};
index 80d97d0..2804b3c 100644 (file)
@@ -96,7 +96,8 @@ WI.TimelineRecordingContentView = class TimelineRecordingContentView extends WI.
         WI.ContentView.addEventListener(WI.ContentView.Event.SelectionPathComponentsDidChange, this._contentViewSelectionPathComponentDidChange, this);
         WI.ContentView.addEventListener(WI.ContentView.Event.SupplementalRepresentedObjectsDidChange, this._contentViewSupplementalRepresentedObjectsDidChange, this);
 
-        WI.TimelineView.addEventListener(WI.TimelineView.Event.RecordWasFiltered, this._recordWasFiltered, this);
+        WI.TimelineView.addEventListener(WI.TimelineView.Event.RecordWasFiltered, this._handleTimelineViewRecordFiltered, this);
+        WI.TimelineView.addEventListener(WI.TimelineView.Event.RecordWasSelected, this._handleTimelineViewRecordSelected, this);
 
         WI.notifications.addEventListener(WI.Notification.VisibilityStateDidChange, this._inspectorVisibilityStateChanged, this);
 
@@ -700,15 +701,7 @@ WI.TimelineRecordingContentView = class TimelineRecordingContentView extends WI.
     {
         let {record} = event.data;
 
-        for (let timelineView of this._timelineViewMap.values()) {
-            let recordMatchesTimeline = record && timelineView.representedObject.type === record.type;
-
-            if (recordMatchesTimeline && timelineView !== this.currentTimelineView)
-                this.showTimelineViewForTimeline(timelineView.representedObject);
-
-            if (!record || recordMatchesTimeline)
-                timelineView.selectRecord(record);
-        }
+        this._selectRecordInTimelineView(record);
     }
 
     _timelineSelected()
@@ -804,7 +797,7 @@ WI.TimelineRecordingContentView = class TimelineRecordingContentView extends WI.
         this.currentTimelineView.updateFilter(this._filterBarNavigationItem.filterBar.filters);
     }
 
-    _recordWasFiltered(event)
+    _handleTimelineViewRecordFiltered(event)
     {
         if (event.target !== this.currentTimelineView)
             return;
@@ -820,6 +813,39 @@ WI.TimelineRecordingContentView = class TimelineRecordingContentView extends WI.
         this._timelineOverview.recordWasFiltered(timeline, record, filtered);
     }
 
+    _handleTimelineViewRecordSelected(event)
+    {
+        if (!this.visible)
+            return;
+
+        let {record} = event.data;
+
+        this._selectRecordInTimelineOverview(record);
+        this._selectRecordInTimelineView(record);
+    }
+
+    _selectRecordInTimelineOverview(record)
+    {
+        let timeline = this._recording.timelineForRecordType(record.type);
+        if (!timeline)
+            return;
+
+        this._timelineOverview.selectRecord(timeline, record);
+    }
+
+    _selectRecordInTimelineView(record)
+    {
+        for (let timelineView of this._timelineViewMap.values()) {
+            let recordMatchesTimeline = record && timelineView.representedObject.type === record.type;
+
+            if (recordMatchesTimeline && timelineView !== this.currentTimelineView)
+                this.showTimelineViewForTimeline(timelineView.representedObject);
+
+            if (!record || recordMatchesTimeline)
+                timelineView.selectRecord(record);
+        }
+    }
+
     _updateProgressView()
     {
         let isCapturing = WI.timelineManager.isCapturing();
index 23842f2..e3c763c 100644 (file)
@@ -335,5 +335,6 @@ WI.TimelineView = class TimelineView extends WI.ContentView
 };
 
 WI.TimelineView.Event = {
-    RecordWasFiltered: "record-was-filtered"
+    RecordWasFiltered: "timeline-view-record-was-filtered",
+    RecordWasSelected: "timeline-view-record-was-selected",
 };