Web Inspector: CPU Usage Timeline - Thread Breakdown
authorjoepeck@webkit.org <joepeck@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 26 Feb 2019 06:36:34 +0000 (06:36 +0000)
committerjoepeck@webkit.org <joepeck@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 26 Feb 2019 06:36:34 +0000 (06:36 +0000)
https://bugs.webkit.org/show_bug.cgi?id=194788

Reviewed by Devin Rousso.

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

* UserInterface/Views/Variables.css:
(:root):
New colors for cpu threads / activity breakdown.

* UserInterface/Models/CPUTimelineRecord.js:
(WI.CPUTimelineRecord.prototype.get workers):
(WI.CPUTimelineRecord):
Distinguish the workers in a CPU timeline record.

* UserInterface/Views/CPUTimelineOverviewGraph.js:
(WI.CPUTimelineOverviewGraph):
(WI.CPUTimelineOverviewGraph.prototype.layout):
* UserInterface/Views/CPUTimelineOverviewGraph.css:
(.timeline-overview-graph.cpu > .stacked-column-chart > svg > rect):
(.timeline-overview-graph.cpu > .stacked-column-chart > svg > rect.main-thread-usage):
(.timeline-overview-graph.cpu > .stacked-column-chart > svg > rect.worker-thread-usage):
(.timeline-overview-graph.cpu > .column-chart > svg > rect):
Stacked column chart for CPU in the overview graph.

* UserInterface/Views/CPUTimelineView.css:
(.timeline-view.cpu > .content > .overview):
(.timeline-view.cpu > .content > .details > .subtitle.threads):
(.timeline-view.cpu > .content > .overview > .chart):
(.timeline-view.cpu > .content > .overview > .chart > .subtitle):
(.timeline-view.cpu > .content > .overview > .chart > .container):
(.timeline-view.cpu > .content > .overview .samples,):
(.timeline-view.cpu .legend):
(.timeline-view.cpu .legend .row):
(.timeline-view.cpu .legend .row + .row):
(.timeline-view.cpu .legend .swatch):
(.timeline-view.cpu .legend > .row > .swatch.sample-type-idle):
(.timeline-view.cpu .legend > .row > .swatch.sample-type-script):
(.timeline-view.cpu .legend > .row > .swatch.sample-type-style):
(.timeline-view.cpu .legend > .row > .swatch.sample-type-layout):
(.timeline-view.cpu .legend > .row > .swatch.sample-type-paint):
(.timeline-view.cpu .circle-chart > svg > path.segment.sample-type-idle):
(.timeline-view.cpu .circle-chart > svg > path.segment.sample-type-script):
(.timeline-view.cpu .circle-chart > svg > path.segment.sample-type-style):
(.timeline-view.cpu .circle-chart > svg > path.segment.sample-type-layout):
(.timeline-view.cpu .circle-chart > svg > path.segment.sample-type-paint):
(.timeline-view.cpu svg > path):
(.timeline-view.cpu .main-thread svg > path,):
(.timeline-view.cpu .worker-thread svg > path,):
(.timeline-view.cpu .cpu-usage-view.empty):
(.timeline-view.cpu :matches(.line-chart, .stacked-line-chart) .markers):
(.timeline-view.cpu :matches(.line-chart, .stacked-line-chart) .markers > div):
(.timeline-view.cpu :matches(.line-chart, .stacked-line-chart) .markers > div > .label):
(.timeline-view.cpu > .content): Deleted.
(.cpu-usage-view .line-chart > svg > path): Deleted.
(.timeline-view.cpu .legend > .row > .swatch.current): Deleted.
* UserInterface/Views/CPUTimelineView.js:
(WI.CPUTimelineView):
(WI.CPUTimelineView.displayNameForSampleType):
(WI.CPUTimelineView.prototype.shown):
(WI.CPUTimelineView.prototype.clear.clearUsageView):
(WI.CPUTimelineView.prototype.clear):
(WI.CPUTimelineView.prototype.initialLayout.createChartContainer):
(WI.CPUTimelineView.prototype.initialLayout.appendLegendRow):
(WI.CPUTimelineView.prototype.initialLayout):
(WI.CPUTimelineView.prototype.layout.removeGreaterThan):
(WI.CPUTimelineView.prototype.layout):
(WI.CPUTimelineView.prototype.layout.layoutView):
(WI.CPUTimelineView.prototype.layout.yScale):
(WI.CPUTimelineView.prototype._computeSamplingData.markRecordEntries):
(WI.CPUTimelineView.prototype._computeSamplingData):
(WI.CPUTimelineView.prototype._removeWorkerThreadViews):
(WI.CPUTimelineView.prototype._clearBreakdownLegend):
(WI.CPUTimelineView.prototype.layout.xScale): Deleted.
Line charts and Circle Chart for threads and breakdowns.

* UserInterface/Views/CPUUsageStackedView.css:
(.cpu-usage-stacked-view):
(.cpu-usage-stacked-view > .details):
(body[dir=ltr] .cpu-usage-stacked-view > .details):
(body[dir=rtl] .cpu-usage-stacked-view > .details):
(.cpu-usage-stacked-view > .details > .name):
(body[dir=rtl] .cpu-usage-stacked-view > .graph):
(.cpu-usage-stacked-view > .graph):
(.cpu-usage-stacked-view > .graph,):
* UserInterface/Views/CPUUsageStackedView.js:
(WI.CPUUsageStackedView):
(WI.CPUUsageStackedView.prototype.get chart):
(WI.CPUUsageStackedView.prototype.clear):
(WI.CPUUsageStackedView.prototype.updateChart):
(WI.CPUUsageStackedView.prototype._updateDetails):
Same as CPUUsageView except Stacked for the total.

* UserInterface/Views/CPUUsageView.css:
(.cpu-usage-view):
(.cpu-usage-view > .details):
(.cpu-usage-view > .details > .name):
(.cpu-usage-view > .graph):
* UserInterface/Views/CPUUsageView.js:
(WI.CPUUsageView):
(WI.CPUUsageView.prototype.get chart):
(WI.CPUUsageView.prototype.clear):
(WI.CPUUsageView.prototype.updateChart):
(WI.CPUUsageView.prototype._updateDetails):
Slight modifications for the new UI.

* UserInterface/Views/LegacyCPUTimelineView.css:
(.timeline-view.legacy-cpu .cpu-usage-view .line-chart > svg > path):
* UserInterface/Views/LegacyCPUTimelineView.js:
(WI.LegacyCPUTimelineView.prototype.layout):
Update API calls in the legacy view for minor changes.

* UserInterface/Views/MemoryCategoryView.css:
(.memory-category-view > .details):
(.memory-category-view > .details > .name):
* UserInterface/Views/MemoryTimelineOverviewGraph.js:
(WI.MemoryTimelineOverviewGraph.prototype.layout):
* UserInterface/Views/MemoryTimelineView.css:
(body .timeline-view.memory):
(.timeline-view.memory): Deleted.
Improvements ported from the CPU timeline views.

* UserInterface/Views/StackedColumnChart.js: Added.
(WI.StackedColumnChart):
(WI.StackedColumnChart.prototype.get size):
(WI.StackedColumnChart.prototype.set size):
(WI.StackedColumnChart.prototype.initializeSections):
(WI.StackedColumnChart.prototype.addColumnSet):
(WI.StackedColumnChart.prototype.clear):
(WI.StackedColumnChart.prototype.layout):
A stacked column chart implementation.

* UserInterface/Views/View.js:
(WI.View.prototype.removeUnparentedSubview):
Add a way to remove a subview that had its `element` moved
someplace other than a direct child of our element.

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

20 files changed:
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js
Source/WebInspectorUI/UserInterface/Main.html
Source/WebInspectorUI/UserInterface/Models/CPUTimelineRecord.js
Source/WebInspectorUI/UserInterface/Views/CPUTimelineOverviewGraph.css
Source/WebInspectorUI/UserInterface/Views/CPUTimelineOverviewGraph.js
Source/WebInspectorUI/UserInterface/Views/CPUTimelineView.css
Source/WebInspectorUI/UserInterface/Views/CPUTimelineView.js
Source/WebInspectorUI/UserInterface/Views/CPUUsageStackedView.css [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/CPUUsageStackedView.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/CPUUsageView.css
Source/WebInspectorUI/UserInterface/Views/CPUUsageView.js
Source/WebInspectorUI/UserInterface/Views/LegacyCPUTimelineView.css
Source/WebInspectorUI/UserInterface/Views/LegacyCPUTimelineView.js
Source/WebInspectorUI/UserInterface/Views/MemoryCategoryView.css
Source/WebInspectorUI/UserInterface/Views/MemoryTimelineOverviewGraph.js
Source/WebInspectorUI/UserInterface/Views/MemoryTimelineView.css
Source/WebInspectorUI/UserInterface/Views/StackedColumnChart.js [new file with mode: 0644]
Source/WebInspectorUI/UserInterface/Views/Variables.css
Source/WebInspectorUI/UserInterface/Views/View.js

index f10e889..75f9b9f 100644 (file)
@@ -1,5 +1,147 @@
 2019-02-25  Joseph Pecoraro  <pecoraro@apple.com>
 
+        Web Inspector: CPU Usage Timeline - Thread Breakdown
+        https://bugs.webkit.org/show_bug.cgi?id=194788
+
+        Reviewed by Devin Rousso.
+
+        * Localizations/en.lproj/localizedStrings.js:
+        * UserInterface/Main.html:
+        New strings and files.
+
+        * UserInterface/Views/Variables.css:
+        (:root):
+        New colors for cpu threads / activity breakdown.
+
+        * UserInterface/Models/CPUTimelineRecord.js:
+        (WI.CPUTimelineRecord.prototype.get workers):
+        (WI.CPUTimelineRecord):
+        Distinguish the workers in a CPU timeline record.
+
+        * UserInterface/Views/CPUTimelineOverviewGraph.js:
+        (WI.CPUTimelineOverviewGraph):
+        (WI.CPUTimelineOverviewGraph.prototype.layout):
+        * UserInterface/Views/CPUTimelineOverviewGraph.css:
+        (.timeline-overview-graph.cpu > .stacked-column-chart > svg > rect):
+        (.timeline-overview-graph.cpu > .stacked-column-chart > svg > rect.main-thread-usage):
+        (.timeline-overview-graph.cpu > .stacked-column-chart > svg > rect.worker-thread-usage):
+        (.timeline-overview-graph.cpu > .column-chart > svg > rect):
+        Stacked column chart for CPU in the overview graph.
+
+        * UserInterface/Views/CPUTimelineView.css:
+        (.timeline-view.cpu > .content > .overview):
+        (.timeline-view.cpu > .content > .details > .subtitle.threads):
+        (.timeline-view.cpu > .content > .overview > .chart):
+        (.timeline-view.cpu > .content > .overview > .chart > .subtitle):
+        (.timeline-view.cpu > .content > .overview > .chart > .container):
+        (.timeline-view.cpu > .content > .overview .samples,):
+        (.timeline-view.cpu .legend):
+        (.timeline-view.cpu .legend .row):
+        (.timeline-view.cpu .legend .row + .row):
+        (.timeline-view.cpu .legend .swatch):
+        (.timeline-view.cpu .legend > .row > .swatch.sample-type-idle):
+        (.timeline-view.cpu .legend > .row > .swatch.sample-type-script):
+        (.timeline-view.cpu .legend > .row > .swatch.sample-type-style):
+        (.timeline-view.cpu .legend > .row > .swatch.sample-type-layout):
+        (.timeline-view.cpu .legend > .row > .swatch.sample-type-paint):
+        (.timeline-view.cpu .circle-chart > svg > path.segment.sample-type-idle):
+        (.timeline-view.cpu .circle-chart > svg > path.segment.sample-type-script):
+        (.timeline-view.cpu .circle-chart > svg > path.segment.sample-type-style):
+        (.timeline-view.cpu .circle-chart > svg > path.segment.sample-type-layout):
+        (.timeline-view.cpu .circle-chart > svg > path.segment.sample-type-paint):
+        (.timeline-view.cpu svg > path):
+        (.timeline-view.cpu .main-thread svg > path,):
+        (.timeline-view.cpu .worker-thread svg > path,):
+        (.timeline-view.cpu .cpu-usage-view.empty):
+        (.timeline-view.cpu :matches(.line-chart, .stacked-line-chart) .markers):
+        (.timeline-view.cpu :matches(.line-chart, .stacked-line-chart) .markers > div):
+        (.timeline-view.cpu :matches(.line-chart, .stacked-line-chart) .markers > div > .label):
+        (.timeline-view.cpu > .content): Deleted.
+        (.cpu-usage-view .line-chart > svg > path): Deleted.
+        (.timeline-view.cpu .legend > .row > .swatch.current): Deleted.
+        * UserInterface/Views/CPUTimelineView.js:
+        (WI.CPUTimelineView):
+        (WI.CPUTimelineView.displayNameForSampleType):
+        (WI.CPUTimelineView.prototype.shown):
+        (WI.CPUTimelineView.prototype.clear.clearUsageView):
+        (WI.CPUTimelineView.prototype.clear):
+        (WI.CPUTimelineView.prototype.initialLayout.createChartContainer):
+        (WI.CPUTimelineView.prototype.initialLayout.appendLegendRow):
+        (WI.CPUTimelineView.prototype.initialLayout):
+        (WI.CPUTimelineView.prototype.layout.removeGreaterThan):
+        (WI.CPUTimelineView.prototype.layout):
+        (WI.CPUTimelineView.prototype.layout.layoutView):
+        (WI.CPUTimelineView.prototype.layout.yScale):
+        (WI.CPUTimelineView.prototype._computeSamplingData.markRecordEntries):
+        (WI.CPUTimelineView.prototype._computeSamplingData):
+        (WI.CPUTimelineView.prototype._removeWorkerThreadViews):
+        (WI.CPUTimelineView.prototype._clearBreakdownLegend):
+        (WI.CPUTimelineView.prototype.layout.xScale): Deleted.
+        Line charts and Circle Chart for threads and breakdowns.
+
+        * UserInterface/Views/CPUUsageStackedView.css:
+        (.cpu-usage-stacked-view):
+        (.cpu-usage-stacked-view > .details):
+        (body[dir=ltr] .cpu-usage-stacked-view > .details):
+        (body[dir=rtl] .cpu-usage-stacked-view > .details):
+        (.cpu-usage-stacked-view > .details > .name):
+        (body[dir=rtl] .cpu-usage-stacked-view > .graph):
+        (.cpu-usage-stacked-view > .graph):
+        (.cpu-usage-stacked-view > .graph,):
+        * UserInterface/Views/CPUUsageStackedView.js:
+        (WI.CPUUsageStackedView):
+        (WI.CPUUsageStackedView.prototype.get chart):
+        (WI.CPUUsageStackedView.prototype.clear):
+        (WI.CPUUsageStackedView.prototype.updateChart):
+        (WI.CPUUsageStackedView.prototype._updateDetails):
+        Same as CPUUsageView except Stacked for the total.
+
+        * UserInterface/Views/CPUUsageView.css:
+        (.cpu-usage-view):
+        (.cpu-usage-view > .details):
+        (.cpu-usage-view > .details > .name):
+        (.cpu-usage-view > .graph):
+        * UserInterface/Views/CPUUsageView.js:
+        (WI.CPUUsageView):
+        (WI.CPUUsageView.prototype.get chart):
+        (WI.CPUUsageView.prototype.clear):
+        (WI.CPUUsageView.prototype.updateChart):
+        (WI.CPUUsageView.prototype._updateDetails):
+        Slight modifications for the new UI.
+
+        * UserInterface/Views/LegacyCPUTimelineView.css:
+        (.timeline-view.legacy-cpu .cpu-usage-view .line-chart > svg > path):
+        * UserInterface/Views/LegacyCPUTimelineView.js:
+        (WI.LegacyCPUTimelineView.prototype.layout):
+        Update API calls in the legacy view for minor changes.
+
+        * UserInterface/Views/MemoryCategoryView.css:
+        (.memory-category-view > .details):
+        (.memory-category-view > .details > .name):
+        * UserInterface/Views/MemoryTimelineOverviewGraph.js:
+        (WI.MemoryTimelineOverviewGraph.prototype.layout):
+        * UserInterface/Views/MemoryTimelineView.css:
+        (body .timeline-view.memory):
+        (.timeline-view.memory): Deleted.
+        Improvements ported from the CPU timeline views.
+
+        * UserInterface/Views/StackedColumnChart.js: Added.
+        (WI.StackedColumnChart):
+        (WI.StackedColumnChart.prototype.get size):
+        (WI.StackedColumnChart.prototype.set size):
+        (WI.StackedColumnChart.prototype.initializeSections):
+        (WI.StackedColumnChart.prototype.addColumnSet):
+        (WI.StackedColumnChart.prototype.clear):
+        (WI.StackedColumnChart.prototype.layout):
+        A stacked column chart implementation.
+
+        * UserInterface/Views/View.js:
+        (WI.View.prototype.removeUnparentedSubview):
+        Add a way to remove a subview that had its `element` moved
+        someplace other than a direct child of our element.
+
+2019-02-25  Joseph Pecoraro  <pecoraro@apple.com>
+
         Web Inspector: Dark Mode: Network Overview Graph segments have distracting white box shadow
         https://bugs.webkit.org/show_bug.cgi?id=194966
 
index 9332b99..3f5c8c8 100644 (file)
@@ -158,6 +158,7 @@ localizedStrings["Break on request with URL:"] = "Break on request with URL:";
 localizedStrings["Break on\u2026"] = "Break on\u2026";
 localizedStrings["Breakdown"] = "Breakdown";
 localizedStrings["Breakdown of each memory category at the end of the selected time range"] = "Breakdown of each memory category at the end of the selected time range";
+localizedStrings["Breakdown of time spent on the main thread"] = "Breakdown of time spent on the main thread";
 localizedStrings["Breakpoint"] = "Breakpoint";
 localizedStrings["Breakpoints"] = "Breakpoints";
 localizedStrings["Breakpoints disabled"] = "Breakpoints disabled";
@@ -599,6 +600,7 @@ localizedStrings["Lowest: %s"] = "Lowest: %s";
 localizedStrings["MIME Type"] = "MIME Type";
 localizedStrings["MIME Type:"] = "MIME Type:";
 localizedStrings["MSE Logging:"] = "MSE Logging:";
+localizedStrings["Main Thread"] = "Main Thread";
 localizedStrings["Manifest URL"] = "Manifest URL";
 localizedStrings["Mass"] = "Mass";
 localizedStrings["Matching"] = "Matching";
@@ -688,6 +690,7 @@ localizedStrings["Originally %s"] = "Originally %s";
 localizedStrings["Originator"] = "Originator";
 localizedStrings["Other"] = "Other";
 localizedStrings["Other Issue"] = "Other Issue";
+localizedStrings["Other Threads"] = "Other Threads";
 localizedStrings["Other\u2026"] = "Other\u2026";
 localizedStrings["Outgoing message"] = "Outgoing message";
 localizedStrings["Output: "] = "Output: ";
@@ -955,6 +958,7 @@ localizedStrings["Stop recording once page loads"] = "Stop recording once page l
 localizedStrings["Stopping the \u201C%s\u201D audit"] = "Stopping the \u201C%s\u201D audit";
 localizedStrings["Storage"] = "Storage";
 localizedStrings["Style Attribute"] = "Style Attribute";
+localizedStrings["Style Resolution"] = "Style Resolution";
 localizedStrings["Style rule"] = "Style rule";
 localizedStrings["Styles"] = "Styles";
 localizedStrings["Styles Invalidated"] = "Styles Invalidated";
@@ -1000,7 +1004,9 @@ localizedStrings["This is what the result of an unsupported test with no data lo
 localizedStrings["This object is a root"] = "This object is a root";
 localizedStrings["This object is referenced by internal objects"] = "This object is referenced by internal objects";
 localizedStrings["This text resource could benefit from compression"] = "This text resource could benefit from compression";
+localizedStrings["Threads"] = "Threads";
 localizedStrings["Time"] = "Time";
+localizedStrings["Time spent on the main thread"] = "Time spent on the main thread";
 localizedStrings["Time to First Byte"] = "Time to First Byte";
 localizedStrings["Timeline"] = "Timeline";
 localizedStrings["Timeline Recording %d"] = "Timeline Recording %d";
@@ -1016,6 +1022,7 @@ localizedStrings["Timing"] = "Timing";
 localizedStrings["Toggle Classes"] = "Toggle Classes";
 localizedStrings["Toggle Visibility"] = "Toggle Visibility";
 localizedStrings["Top Functions"] = "Top Functions";
+localizedStrings["Total"] = "Total";
 localizedStrings["Total Time"] = "Total Time";
 localizedStrings["Total memory size at the end of the selected time range"] = "Total memory size at the end of the selected time range";
 localizedStrings["Total time"] = "Total time";
@@ -1074,6 +1081,7 @@ localizedStrings["Warnings"] = "Warnings";
 localizedStrings["Watch Expressions"] = "Watch Expressions";
 localizedStrings["Waterfall"] = "Waterfall";
 localizedStrings["Web Inspector"] = "Web Inspector";
+localizedStrings["WebKit Threads"] = "WebKit Threads";
 localizedStrings["WebRTC"] = "WebRTC";
 localizedStrings["WebRTC Logging:"] = "WebRTC Logging:";
 localizedStrings["WebSocket Connection Established"] = "WebSocket Connection Established";
@@ -1081,6 +1089,7 @@ localizedStrings["Whitespace characters"] = "Whitespace characters";
 localizedStrings["Width"] = "Width";
 localizedStrings["With Object Properties"] = "With Object Properties";
 localizedStrings["Worker"] = "Worker";
+localizedStrings["Worker Thread"] = "Worker Thread";
 localizedStrings["Worker \u2014 %s"] = "Worker \u2014 %s";
 localizedStrings["Working Copy"] = "Working Copy";
 localizedStrings["Wrap lines to editor width"] = "Wrap lines to editor width";
index 0c53f97..a09b30a 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/CPUUsageStackedView.css">
     <link rel="stylesheet" href="Views/CPUUsageView.css">
     <link rel="stylesheet" href="Views/CallFrameIcons.css">
     <link rel="stylesheet" href="Views/CallFrameTreeElement.css">
     <script src="Views/ButtonToolbarItem.js"></script>
     <script src="Views/CPUTimelineOverviewGraph.js"></script>
     <script src="Views/CPUTimelineView.js"></script>
+    <script src="Views/CPUUsageStackedView.js"></script>
     <script src="Views/CPUUsageView.js"></script>
     <script src="Views/CSSStyleSheetTreeElement.js"></script>
     <script src="Views/CallFrameTreeElement.js"></script>
     <script src="Views/SpringEditor.js"></script>
     <script src="Views/SVGImageResourceClusterContentView.js"></script>
     <script src="Views/StackTraceView.js"></script>
+    <script src="Views/StackedColumnChart.js"></script>
     <script src="Views/StackedLineChart.js"></script>
     <script src="Views/StorageSidebarPanel.js"></script>
     <script src="Views/SyntaxHighlightingSupport.js"></script>
index 51ea2ae..ea07bfc 100644 (file)
@@ -43,6 +43,7 @@ WI.CPUTimelineRecord = class CPUTimelineRecord extends WI.TimelineRecord
         this._webkitThreadUsage = 0;
         this._workerThreadUsage = 0;
         this._unknownThreadUsage = 0;
+        this._workersData = null;
 
         for (let thread of threads) {
             if (thread.type === InspectorBackend.domains.CPUProfiler.ThreadInfoType.Main) {
@@ -52,10 +53,15 @@ WI.CPUTimelineRecord = class CPUTimelineRecord extends WI.TimelineRecord
             }
 
             if (thread.type === InspectorBackend.domains.CPUProfiler.ThreadInfoType.WebKit) {
-                if (thread.targetId)
+                if (thread.targetId) {
+                    if (!this._workersData)
+                        this._workersData = [];
+                    this._workersData.push(thread);
                     this._workerThreadUsage += thread.usage;
-                else
-                    this._webkitThreadUsage += thread.usage;
+                    continue;
+                }
+
+                this._webkitThreadUsage += thread.usage;
                 continue;
             }
 
@@ -72,4 +78,5 @@ WI.CPUTimelineRecord = class CPUTimelineRecord extends WI.TimelineRecord
     get webkitThreadUsage() { return this._webkitThreadUsage; }
     get workerThreadUsage() { return this._workerThreadUsage; }
     get unknownThreadUsage() { return this._unknownThreadUsage; }
+    get workersData() { return this._workersData; }
 };
index 31b28db..2e00451 100644 (file)
@@ -57,11 +57,29 @@ body[dir=rtl] .timeline-overview-graph.cpu > .legend {
     background-color: var(--timeline-even-background-color);
 }
 
-body[dir=rtl] .timeline-overview-graph.cpu > .column-chart {
+body[dir=rtl] .timeline-overview-graph.cpu > .stacked-column-chart {
     transform: scaleX(-1);
 }
 
-.timeline-overview-graph.cpu > .column-chart > svg > rect {
+.timeline-overview-graph.cpu > .stacked-column-chart > svg > rect {
     stroke: var(--cpu-stroke-color);
     fill: var(--cpu-fill-color);
 }
+
+.timeline-overview-graph.cpu > .stacked-column-chart > svg > rect.main-thread-usage {
+    fill: var(--cpu-main-thread-fill-color);
+}
+
+.timeline-overview-graph.cpu > .stacked-column-chart > svg > rect.worker-thread-usage {
+    fill: var(--cpu-worker-thread-fill-color);
+}
+
+/* LegacyCPUTimeline */
+.timeline-overview-graph.cpu > .column-chart > svg > rect {
+    stroke: var(--cpu-stroke-color);
+    fill: var(--cpu-main-thread-fill-color);
+}
+
+body[dir=rtl] .timeline-overview-graph.cpu > .column-chart {
+    transform: scaleX(-1);
+}
index 221d88f..1ddb3fa 100644 (file)
@@ -38,7 +38,11 @@ WI.CPUTimelineOverviewGraph = class CPUTimelineOverviewGraph extends WI.Timeline
         this._cpuTimeline.addEventListener(WI.Timeline.Event.RecordAdded, this._cpuTimelineRecordAdded, this);
 
         let size = new WI.Size(0, this.height);
-        this._chart = new WI.ColumnChart(size);
+        if (WI.settings.experimentalEnableCPUUsageEnhancements.value) {
+            this._chart = new WI.StackedColumnChart(size);
+            this._chart.initializeSections(["main-thread-usage", "worker-thread-usage", "total-usage"]);
+        } else
+            this._chart = new WI.ColumnChart(size);
         this.addSubview(this._chart);
         this.element.appendChild(this._chart.element);
 
@@ -114,13 +118,15 @@ WI.CPUTimelineOverviewGraph = class CPUTimelineOverviewGraph extends WI.Timeline
         // Bars for each record.
         for (let record of visibleRecords) {
             let w = intervalWidth;
-            let h = Math.max(minimumDisplayHeight, yScale(record.usage));
+            let h3 = Math.max(minimumDisplayHeight, yScale(record.usage));
             let x = xScale(record.startTime - (samplingRatePerSecond / 2));
-            let y = height - h;
-            this._chart.addColumn(x, y, w, h);
+            if (WI.settings.experimentalEnableCPUUsageEnhancements.value) {
+                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]);
+            } else
+                this._chart.addColumn(x, height - h3, w, h3);
         }
-
-        this._chart.updateLayout();
     }
 
     // Private
index ef4c78e..bab51d7 100644 (file)
@@ -27,8 +27,12 @@ body .timeline-view.cpu {
     overflow: scroll;
 }
 
-.timeline-view.cpu > .content {
-    margin-top: 10px;
+.timeline-view.cpu > .content > .overview {
+    display: flex;
+    justify-content: center;
+    margin-bottom: 10px;
+    padding: 10px;
+    border-bottom: 1px solid var(--border-color);
 }
 
 .timeline-view.cpu > .content .subtitle {
@@ -63,12 +67,150 @@ body[dir=rtl] .timeline-view.cpu > .content > .details > .timeline-ruler {
     border-bottom: 1px solid var(--border-color);
 }
 
-.cpu-usage-view .line-chart > svg > path {
+.timeline-view.cpu > .content > .details > .subtitle.threads {
+    position: relative;
+    z-index: calc(var(--timeline-marker-z-index) + 1);
+    padding-top: 10px;
+    background-color: var(--background-color-content);
+}
+
+.timeline-view.cpu > .content > .overview > .chart {
+    width: 420px;
+    text-align: center;
+}
+
+.timeline-view.cpu > .content > .overview > .chart > .subtitle {
+    margin-bottom: 1em;
+}
+
+.timeline-view.cpu > .content > .overview > .chart > .container {
+    display: flex;
+    justify-content: center;
+}
+
+.timeline-view.cpu > .content > .overview .samples,
+.timeline-view.cpu > .content > .overview .legend .size {
+    margin: auto;
+    color: var(--text-color-secondary);
+}
+
+.timeline-view.cpu .legend {
+    -webkit-padding-start: 20px;
+    text-align: start;
+}
+
+.timeline-view.cpu .legend .row {
+    display: flex;
+}
+
+.timeline-view.cpu .legend .row + .row {
+    margin-top: 4px;
+}
+
+.timeline-view.cpu .legend .swatch {
+    width: 1em;
+    height: 1em;
+    margin-top: 1px;
+    -webkit-margin-end: 8px;
+}
+
+.timeline-view.cpu .legend > .row > .swatch.sample-type-idle {
+    border: 1px solid var(--cpu-idle-stroke-color);
+    background-color: var(--cpu-idle-fill-color);
+}
+
+.timeline-view.cpu .legend > .row > .swatch.sample-type-script {
+    border: 1px solid var(--cpu-script-stroke-color);
+    background-color: var(--cpu-script-fill-color);
+}
+
+.timeline-view.cpu .legend > .row > .swatch.sample-type-style {
+    border: 1px solid var(--cpu-style-stroke-color);
+    background-color: var(--cpu-style-fill-color);
+}
+
+.timeline-view.cpu .legend > .row > .swatch.sample-type-layout {
+    border: 1px solid var(--cpu-layout-stroke-color);
+    background-color: var(--cpu-layout-fill-color);
+}
+
+.timeline-view.cpu .legend > .row > .swatch.sample-type-paint {
+    border: 1px solid var(--cpu-paint-stroke-color);
+    background-color: var(--cpu-paint-fill-color);
+}
+
+.timeline-view.cpu .circle-chart > svg > path.segment.sample-type-idle {
+    stroke: var(--cpu-idle-stroke-color);
+    fill: var(--cpu-idle-fill-color);
+}
+
+.timeline-view.cpu .circle-chart > svg > path.segment.sample-type-script {
+    stroke: var(--cpu-script-stroke-color);
+    fill: var(--cpu-script-fill-color);
+}
+
+.timeline-view.cpu .circle-chart > svg > path.segment.sample-type-style {
+    stroke: var(--cpu-style-stroke-color);
+    fill: var(--cpu-style-fill-color);
+}
+
+.timeline-view.cpu .circle-chart > svg > path.segment.sample-type-layout {
+    stroke: var(--cpu-layout-stroke-color);
+    fill: var(--cpu-layout-fill-color);
+}
+
+.timeline-view.cpu .circle-chart > svg > path.segment.sample-type-paint {
+    stroke: var(--cpu-paint-stroke-color);
+    fill: var(--cpu-paint-fill-color);
+}
+
+.timeline-view.cpu svg > path {
     stroke: var(--cpu-stroke-color);
     fill: var(--cpu-fill-color);
 }
 
-.timeline-view.cpu .legend > .row > .swatch.current {
-    border: 1px solid var(--cpu-max-comparison-stroke-color);
-    background: var(--cpu-max-comparison-fill-color);
+.timeline-view.cpu .main-thread svg > path,
+.timeline-view.cpu svg > path.main-thread-usage {
+    fill: var(--cpu-main-thread-fill-color);
+}
+
+.timeline-view.cpu .worker-thread svg > path,
+.timeline-view.cpu svg > path.worker-thread-usage {
+    fill: var(--cpu-worker-thread-fill-color);
+}
+
+.timeline-view.cpu .cpu-usage-view.empty {
+    display: none;
+}
+
+.timeline-view.cpu :matches(.line-chart, .stacked-line-chart) .markers {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    pointer-events: none;
+}
+
+.timeline-view.cpu :matches(.line-chart, .stacked-line-chart) .markers > div {
+    position: absolute;
+    z-index: 10;
+    width: 100%;
+    height: 1px;
+    background-color: hsla(0, 0%, var(--foreground-lightness), 0.07);
+}
+
+body[dir=ltr] .timeline-view.cpu :matches(.line-chart, .stacked-line-chart) .markers > div {
+    text-align: end;
+}
+
+body[dir=rtl] .timeline-view.cpu :matches(.line-chart, .stacked-line-chart) .markers > div {
+    transform: scaleX(-1);
+}
+
+.timeline-view.cpu :matches(.line-chart, .stacked-line-chart) .markers > div > .label {
+    padding: 2px;
+    font-size: 8px;
+    color: var(--text-color-secondary);
+    background-color: var(--background-color-content);
 }
index 34e3183..4e6795f 100644 (file)
@@ -35,36 +35,37 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
 
         this.element.classList.add("cpu");
 
-        let contentElement = this.element.appendChild(document.createElement("div"));
-        contentElement.classList.add("content");
-
-        // FIXME: Overview with charts.
-
-        let detailsContainerElement = this._detailsContainerElement = contentElement.appendChild(document.createElement("div"));
-        detailsContainerElement.classList.add("details");
-
-        this._timelineRuler = new WI.TimelineRuler;
-        this.addSubview(this._timelineRuler);
-        detailsContainerElement.appendChild(this._timelineRuler.element);
+        timeline.addEventListener(WI.Timeline.Event.RecordAdded, this._cpuTimelineRecordAdded, this);
+    }
 
-        let detailsSubtitleElement = detailsContainerElement.appendChild(document.createElement("div"));
-        detailsSubtitleElement.classList.add("subtitle");
-        detailsSubtitleElement.textContent = WI.UIString("CPU Usage");
+    // Static
 
-        this._cpuUsageView = new WI.CPUUsageView;
-        this.addSubview(this._cpuUsageView);
-        this._detailsContainerElement.appendChild(this._cpuUsageView.element);
-
-        timeline.addEventListener(WI.Timeline.Event.RecordAdded, this._cpuTimelineRecordAdded, this);
+    static displayNameForSampleType(type)
+    {
+        switch (type) {
+        case WI.CPUTimelineView.SampleType.Script:
+            return WI.UIString("Script");
+        case WI.CPUTimelineView.SampleType.Layout:
+            return WI.UIString("Layout");
+        case WI.CPUTimelineView.SampleType.Paint:
+            return WI.UIString("Paint");
+        case WI.CPUTimelineView.SampleType.Style:
+            return WI.UIString("Style Resolution");
+        }
+        console.error("Unknown sample type", type);
     }
 
+    static get cpuUsageViewHeight() { return 150; }
+    static get threadCPUUsageViewHeight() { return 65; }
+
     // Public
 
     shown()
     {
         super.shown();
 
-        this._timelineRuler.updateLayout(WI.View.LayoutReason.Resize);
+        if (this._timelineRuler)
+            this._timelineRuler.updateLayout(WI.View.LayoutReason.Resize);
     }
 
     closed()
@@ -82,7 +83,27 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
 
     clear()
     {
-        this._cpuUsageView.clear();
+        if (!this.didInitialLayout)
+            return;
+
+        this._breakdownChart.clear();
+        this._breakdownChart.needsLayout();
+        this._clearBreakdownLegend();
+
+        function clearUsageView(view) {
+            view.clear();
+
+            let markersElement = view.chart.element.querySelector(".markers");
+            if (markersElement)
+                markersElement.remove();
+        }
+
+        clearUsageView(this._cpuUsageView);
+        clearUsageView(this._mainThreadUsageView);
+        clearUsageView(this._webkitThreadUsageView);
+        clearUsageView(this._unknownThreadUsageView);
+
+        this._removeWorkerThreadViews();
     }
 
     get scrollableElements()
@@ -94,6 +115,110 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
 
     get showsFilterBar() { return false; }
 
+    initialLayout()
+    {
+        this.element.style.setProperty("--cpu-usage-stacked-view-height", CPUTimelineView.cpuUsageViewHeight + "px");
+        this.element.style.setProperty("--cpu-usage-view-height", CPUTimelineView.threadCPUUsageViewHeight + "px");
+
+        let contentElement = this.element.appendChild(document.createElement("div"));
+        contentElement.classList.add("content");
+
+        let overviewElement = contentElement.appendChild(document.createElement("div"));
+        overviewElement.classList.add("overview");
+
+        function createChartContainer(parentElement, subtitle, tooltip) {
+            let chartElement = parentElement.appendChild(document.createElement("div"));
+            chartElement.classList.add("chart");
+
+            let chartSubtitleElement = chartElement.appendChild(document.createElement("div"));
+            chartSubtitleElement.classList.add("subtitle");
+            chartSubtitleElement.textContent = subtitle;
+            chartSubtitleElement.title = tooltip;
+
+            let chartFlexContainerElement = chartElement.appendChild(document.createElement("div"));
+            chartFlexContainerElement.classList.add("container");
+            return chartFlexContainerElement;
+        }
+
+        function appendLegendRow(legendElement, sampleType) {
+            let rowElement = legendElement.appendChild(document.createElement("div"));
+            rowElement.classList.add("row");
+
+            let swatchElement = rowElement.appendChild(document.createElement("div"));
+            swatchElement.classList.add("swatch", sampleType);
+
+            let valueContainer = rowElement.appendChild(document.createElement("div"));
+            valueContainer.classList.add("value");
+
+            let labelElement = valueContainer.appendChild(document.createElement("div"));
+            labelElement.classList.add("label");
+            labelElement.textContent = WI.CPUTimelineView.displayNameForSampleType(sampleType);
+
+            let sizeElement = valueContainer.appendChild(document.createElement("div"));
+            sizeElement.classList.add("size");
+
+            return sizeElement;
+        }
+
+        let breakdownChartContainerElement = createChartContainer(overviewElement, WI.UIString("Main Thread"), WI.UIString("Breakdown of time spent on the main thread"));
+        this._breakdownChart = new WI.CircleChart({size: 120, innerRadiusRatio: 0.5});
+        this._breakdownChart.segments = Object.values(WI.CPUTimelineView.SampleType);
+        this.addSubview(this._breakdownChart);
+        breakdownChartContainerElement.appendChild(this._breakdownChart.element);
+
+        this._breakdownLegendElement = breakdownChartContainerElement.appendChild(document.createElement("div"));
+        this._breakdownLegendElement.classList.add("legend");
+
+        this._breakdownLegendScriptElement = appendLegendRow(this._breakdownLegendElement, WI.CPUTimelineView.SampleType.Script);
+        this._breakdownLegendLayoutElement = appendLegendRow(this._breakdownLegendElement, WI.CPUTimelineView.SampleType.Layout);
+        this._breakdownLegendPaintElement = appendLegendRow(this._breakdownLegendElement, WI.CPUTimelineView.SampleType.Paint);
+        this._breakdownLegendStyleElement = appendLegendRow(this._breakdownLegendElement, WI.CPUTimelineView.SampleType.Style);
+
+        let detailsContainerElement = this._detailsContainerElement = contentElement.appendChild(document.createElement("div"));
+        detailsContainerElement.classList.add("details");
+
+        this._timelineRuler = new WI.TimelineRuler;
+        this._timelineRuler.zeroTime = this.zeroTime;
+        this._timelineRuler.startTime = this.startTime;
+        this._timelineRuler.endTime = this.endTime;
+
+        this.addSubview(this._timelineRuler);
+        detailsContainerElement.appendChild(this._timelineRuler.element);
+
+        // Cause the TimelineRuler to layout now so we will have some of its
+        // important properties initialized for our layout.
+        this._timelineRuler.updateLayout(WI.View.LayoutReason.Resize);
+
+        let detailsSubtitleElement = detailsContainerElement.appendChild(document.createElement("div"));
+        detailsSubtitleElement.classList.add("subtitle");
+        detailsSubtitleElement.textContent = WI.UIString("CPU Usage");
+
+        this._cpuUsageView = new WI.CPUUsageStackedView(WI.UIString("Total"));
+        this.addSubview(this._cpuUsageView);
+        this._detailsContainerElement.appendChild(this._cpuUsageView.element);
+
+        let threadsSubtitleElement = detailsContainerElement.appendChild(document.createElement("div"));
+        threadsSubtitleElement.classList.add("subtitle", "threads");
+        threadsSubtitleElement.textContent = WI.UIString("Threads");
+
+        this._mainThreadUsageView = new WI.CPUUsageView(WI.UIString("Main Thread"));
+        this._mainThreadUsageView.element.classList.add("main-thread");
+        this.addSubview(this._mainThreadUsageView);
+        this._detailsContainerElement.appendChild(this._mainThreadUsageView.element);
+
+        this._webkitThreadUsageView = new WI.CPUUsageView(WI.UIString("WebKit Threads"));
+        this._webkitThreadUsageView.element.classList.add("non-main-thread");
+        this.addSubview(this._webkitThreadUsageView);
+        this._detailsContainerElement.appendChild(this._webkitThreadUsageView.element);
+
+        this._unknownThreadUsageView = new WI.CPUUsageView(WI.UIString("Other Threads"));
+        this._unknownThreadUsageView.element.classList.add("non-main-thread");
+        this.addSubview(this._unknownThreadUsageView);
+        this._detailsContainerElement.appendChild(this._unknownThreadUsageView.element);
+
+        this._workerViews = [];
+    }
+
     layout()
     {
         if (this.layoutReason === WI.View.LayoutReason.Resize)
@@ -104,14 +229,16 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
         this._timelineRuler.startTime = this.startTime;
         this._timelineRuler.endTime = this.endTime;
 
-        const cpuUsageViewHeight = 75; // Keep this in sync with .cpu-usage-view
+        let secondsPerPixel = this._timelineRuler.secondsPerPixel;
+        if (!secondsPerPixel)
+            return;
 
         let graphStartTime = this.startTime;
         let graphEndTime = this.endTime;
-        let secondsPerPixel = this._timelineRuler.secondsPerPixel;
         let visibleEndTime = Math.min(this.endTime, this.currentTime);
 
         let discontinuities = this._recording.discontinuitiesInTimeRange(graphStartTime, visibleEndTime);
+        let originalDiscontinuities = discontinuities.slice();
 
         // Don't include the record before the graph start if the graph start is within a gap.
         let includeRecordBeforeStart = !discontinuities.length || discontinuities[0].startTime > graphStartTime;
@@ -121,72 +248,449 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
             return;
         }
 
-        // Update total usage chart with the last record's data.
-        let lastRecord = visibleRecords.lastValue;
+        let samplingData = this._computeSamplingData(graphStartTime, visibleEndTime);
+        let nonIdleSamplesCount = samplingData.samples.length - samplingData.samplesIdle;
+        if (!nonIdleSamplesCount) {
+            this._breakdownChart.clear();
+            this._breakdownChart.needsLayout();
+            this._clearBreakdownLegend();
+        } else {
+            let percentScript = samplingData.samplesScript / nonIdleSamplesCount;
+            let percentLayout = samplingData.samplesLayout / nonIdleSamplesCount;
+            let percentPaint = samplingData.samplesPaint / nonIdleSamplesCount;
+            let percentStyle = samplingData.samplesStyle / nonIdleSamplesCount;
+
+            this._breakdownLegendScriptElement.textContent = `${Number.percentageString(percentScript)} (${samplingData.samplesScript})`;
+            this._breakdownLegendLayoutElement.textContent = `${Number.percentageString(percentLayout)} (${samplingData.samplesLayout})`;
+            this._breakdownLegendPaintElement.textContent = `${Number.percentageString(percentPaint)} (${samplingData.samplesPaint})`;
+            this._breakdownLegendStyleElement.textContent = `${Number.percentageString(percentStyle)} (${samplingData.samplesStyle})`;
+
+            this._breakdownChart.values = [percentScript * 100, percentLayout * 100, percentPaint * 100, percentStyle * 100];
+            this._breakdownChart.needsLayout();
+
+            let centerElement = this._breakdownChart.centerElement;
+            let samplesElement = centerElement.firstChild;
+            if (!samplesElement) {
+                samplesElement = centerElement.appendChild(document.createElement("div"));
+                samplesElement.classList.add("samples");
+                samplesElement.title = WI.UIString("Time spent on the main thread");
+            }
 
-        // FIXME: Left chart.
-        // FIXME: Right chart.
+            let millisecondsStringNoDecimal = WI.UIString("%.0fms").format(nonIdleSamplesCount);
+            samplesElement.textContent = millisecondsStringNoDecimal;
+        }
 
         let dataPoints = [];
+        let workersDataMap = new Map;
+
         let max = -Infinity;
+        let mainThreadMax = -Infinity;
+        let webkitThreadMax = -Infinity;
+        let unknownThreadMax = -Infinity;
+
         let min = Infinity;
+        let mainThreadMin = Infinity;
+        let webkitThreadMin = Infinity;
+        let unknownThreadMin = Infinity;
+
         let average = 0;
+        let mainThreadAverage = 0;
+        let webkitThreadAverage = 0;
+        let unknownThreadAverage = 0;
 
         for (let record of visibleRecords) {
             let time = record.startTime;
-            let usage = record.usage;
+            let {usage, mainThreadUsage, workerThreadUsage, webkitThreadUsage, unknownThreadUsage} = record;
 
             if (discontinuities.length && discontinuities[0].endTime < time) {
                 let startDiscontinuity = discontinuities.shift();
                 let endDiscontinuity = startDiscontinuity;
                 while (discontinuities.length && discontinuities[0].endTime < time)
                     endDiscontinuity = discontinuities.shift();
-                dataPoints.push({time: startDiscontinuity.startTime, size: 0});
-                dataPoints.push({time: endDiscontinuity.endTime, size: 0});
-                dataPoints.push({time: endDiscontinuity.endTime, size: usage});
+
+                if (dataPoints.length) {
+                    let previousDataPoint = dataPoints.lastValue;
+                    dataPoints.push({
+                        time: startDiscontinuity.startTime,
+                        mainThreadUsage: previousDataPoint.mainThreadUsage,
+                        workerThreadUsage: previousDataPoint.workerThreadUsage,
+                        webkitThreadUsage: previousDataPoint.webkitThreadUsage,
+                        unknownThreadUsage: previousDataPoint.unknownThreadUsage,
+                        usage: previousDataPoint.usage,
+                    });
+                }
+
+                dataPoints.push({time: startDiscontinuity.startTime, mainThreadUsage: 0, workerThreadUsage: 0, webkitThreadUsage: 0, unknownThreadUsage: 0, usage: 0});
+                dataPoints.push({time: endDiscontinuity.endTime, mainThreadUsage: 0, workerThreadUsage: 0, webkitThreadUsage: 0, unknownThreadUsage: 0, usage: 0});
+                dataPoints.push({time: endDiscontinuity.endTime, mainThreadUsage, workerThreadUsage, webkitThreadUsage, unknownThreadUsage, usage});
             }
 
-            dataPoints.push({time, size: usage});
+            dataPoints.push({time, mainThreadUsage, workerThreadUsage, webkitThreadUsage, unknownThreadUsage, usage});
+
             max = Math.max(max, usage);
+            mainThreadMax = Math.max(mainThreadMax, mainThreadUsage);
+            webkitThreadMax = Math.max(webkitThreadMax, webkitThreadUsage);
+            unknownThreadMax = Math.max(unknownThreadMax, unknownThreadUsage);
+
             min = Math.min(min, usage);
+            mainThreadMin = Math.min(mainThreadMin, mainThreadUsage);
+            webkitThreadMin = Math.min(webkitThreadMin, webkitThreadUsage);
+            unknownThreadMin = Math.min(unknownThreadMin, unknownThreadUsage);
+
             average += usage;
+            mainThreadAverage += mainThreadUsage;
+            webkitThreadAverage += webkitThreadUsage;
+            unknownThreadAverage += unknownThreadUsage;
+
+            if (record.workersData && record.workersData.length) {
+                for (let {targetId, usage} of record.workersData) {
+                    let workerData = workersDataMap.get(targetId);
+                    if (!workerData) {
+                        workerData = {
+                            discontinuities: originalDiscontinuities.slice(),
+                            recordsCount: 0,
+                            dataPoints: [],
+                            min: Infinity,
+                            max: -Infinity,
+                            average: 0
+                        };
+
+                        while (workerData.discontinuities.length && workerData.discontinuities[0].endTime <= graphStartTime)
+                            workerData.discontinuities.shift();
+                        workerData.dataPoints.push({time: graphStartTime, usage: 0});
+                        workerData.dataPoints.push({time, usage: 0});
+                        workersDataMap.set(targetId, workerData);
+                    }
+
+                    if (workerData.discontinuities.length && workerData.discontinuities[0].endTime < time) {
+                        let startDiscontinuity = workerData.discontinuities.shift();
+                        let endDiscontinuity = startDiscontinuity;
+                        while (workerData.discontinuities.length && workerData.discontinuities[0].endTime < time)
+                            endDiscontinuity = workerData.discontinuities.shift();
+                        if (workerData.dataPoints.length) {
+                            let previousDataPoint = workerData.dataPoints.lastValue;
+                            workerData.dataPoints.push({time: startDiscontinuity.startTime, usage: previousDataPoint.usage});
+                        }
+                        workerData.dataPoints.push({time: startDiscontinuity.startTime, usage: 0});
+                        workerData.dataPoints.push({time: endDiscontinuity.endTime, usage: 0});
+                        workerData.dataPoints.push({time: endDiscontinuity.endTime, usage});
+                    }
+
+                    workerData.dataPoints.push({time, usage});
+                    workerData.recordsCount += 1;
+                    workerData.max = Math.max(workerData.max, usage);
+                    workerData.min = Math.min(workerData.min, usage);
+                    workerData.average += usage;
+                }
+            }
         }
 
         average /= visibleRecords.length;
+        mainThreadAverage /= visibleRecords.length;
+        webkitThreadAverage /= visibleRecords.length;
+        unknownThreadAverage /= visibleRecords.length;
+
+        for (let [workerId, workerData] of workersDataMap)
+            workerData.average = workerData.average / workerData.recordsCount;
 
         // If the graph end time is inside a gap, the last data point should
         // only be extended to the start of the discontinuity.
         if (discontinuities.length)
             visibleEndTime = discontinuities[0].startTime;
 
-        function layoutView(view, {dataPoints, min, max, average}) {
+        function removeGreaterThan(arr, max) {
+            return arr.filter((x) => x <= max);
+        }
+
+        function markerValuesForMaxValue(max) {
+            if (max < 1)
+                return [0.5];
+            if (max < 7)
+                return removeGreaterThan([1, 3, 5], max);
+            if (max < 12.5)
+                return removeGreaterThan([5, 10], max);
+            if (max < 20)
+                return removeGreaterThan([5, 10, 15], max);
+            if (max < 30)
+                return removeGreaterThan([10, 20, 30], max);
+            if (max < 50)
+                return removeGreaterThan([15, 30, 45], max);
+            if (max < 100)
+                return removeGreaterThan([25, 50, 75], max);
+            if (max < 200)
+                return removeGreaterThan([50, 100, 150], max);
+            if (max >= 200) {
+                let hundreds = Math.floor(max / 100);
+                let even = (hundreds % 2) === 0;
+                if (even) {
+                    let top = hundreds * 100;
+                    let bottom = top / 2;
+                    return [bottom, top];
+                }
+                let top = hundreds * 100;
+                let bottom = 100;
+                let mid = (top + bottom) / 2;
+                return [bottom, mid, top];
+            }
+        }
+
+        // Layout all graphs to the same time scale. The maximum value is
+        // the maximum total CPU usage across all threads.
+        let layoutMax = max;
+
+        function layoutView(view, property, graphHeight, {dataPoints, min, max, average}) {
             if (min === Infinity)
                 min = 0;
             if (max === -Infinity)
                 max = 0;
+            if (layoutMax === -Infinity)
+                layoutMax = 0;
+
+            let isAllThreadsGraph = property === null;
 
-            // Zoom in to the top of each graph to accentuate small changes.
-            let graphMin = min * 0.95;
-            let graphMax = (max * 1.05) - graphMin;
+            let graphMax = layoutMax * 1.05;
 
             function xScale(time) {
                 return (time - graphStartTime) / secondsPerPixel;
             }
 
-            let size = new WI.Size(xScale(graphEndTime), cpuUsageViewHeight);
+            let size = new WI.Size(xScale(graphEndTime), graphHeight);
 
             function yScale(value) {
-                return size.height - (((value - graphMin) / graphMax) * size.height);
+                return size.height - ((value / graphMax) * size.height);
+            }
+
+            view.updateChart(dataPoints, size, visibleEndTime, min, max, average, xScale, yScale, property);
+
+            let markersElement = view.chart.element.querySelector(".markers");
+            if (!markersElement) {
+                markersElement = view.chart.element.appendChild(document.createElement("div"));
+                markersElement.className = "markers";
+            }
+            markersElement.removeChildren();
+
+            let markerValues;
+            if (isAllThreadsGraph)
+                markerValues = markerValuesForMaxValue(max);
+            else {
+                const minimumMarkerTextHeight = 17;
+                let percentPerPixel = 1 / (graphHeight / layoutMax);
+                if (layoutMax < 5) {
+                    let minimumDisplayablePercentByTwo = Math.ceil((minimumMarkerTextHeight * percentPerPixel) / 2) * 2;
+                    markerValues = [Math.max(minimumDisplayablePercentByTwo, Math.floor(max))];
+                } else {
+                    let minimumDisplayablePercentByFive = Math.ceil((minimumMarkerTextHeight * percentPerPixel) / 5) * 5;
+                    markerValues = [Math.max(minimumDisplayablePercentByFive, Math.floor(max))];
+                }
             }
 
-            view.updateChart(dataPoints, size, visibleEndTime, min, max, average, xScale, yScale);
+            for (let value of markerValues) {
+                let marginTop = yScale(value);
+
+                let markerElement = markersElement.appendChild(document.createElement("div"));
+                markerElement.style.marginTop = marginTop.toFixed(2) + "px";
+
+                let labelElement = markerElement.appendChild(document.createElement("span"));
+                labelElement.classList.add("label");
+                const precision = 0;
+                labelElement.innerText = Number.percentageString(value / 100, precision);
+            }
         }
 
-        layoutView(this._cpuUsageView, {dataPoints, min, max, average});
+        layoutView(this._cpuUsageView, null, CPUTimelineView.cpuUsageViewHeight, {dataPoints, min, max, average});
+        layoutView(this._mainThreadUsageView, "mainThreadUsage", CPUTimelineView.threadCPUUsageViewHeight, {dataPoints, min: mainThreadMin, max: mainThreadMax, average: mainThreadAverage});
+        layoutView(this._webkitThreadUsageView, "webkitThreadUsage", CPUTimelineView.threadCPUUsageViewHeight, {dataPoints, min: webkitThreadMin, max: webkitThreadMax, average: webkitThreadAverage});
+        layoutView(this._unknownThreadUsageView, "unknownThreadUsage", CPUTimelineView.threadCPUUsageViewHeight, {dataPoints, min: unknownThreadMin, max: unknownThreadMax, average: unknownThreadAverage});
+
+        this._removeWorkerThreadViews();
+
+        for (let [workerId, workerData] of workersDataMap) {
+            let worker = WI.targetManager.targetForIdentifier(workerId);
+            let displayName = worker ? worker.displayName : WI.UIString("Worker Thread");
+            let workerView = new WI.CPUUsageView(displayName);
+            workerView.element.classList.add("worker-thread");
+            this.addSubview(workerView);
+            this._detailsContainerElement.insertBefore(workerView.element, this._webkitThreadUsageView.element);
+            this._workerViews.push(workerView);
+
+            layoutView(workerView, "usage", CPUTimelineView.threadCPUUsageViewHeight, {dataPoints: workerData.dataPoints, min: workerData.min, max: workerData.max, average: workerData.average});
+        }
     }
 
     // Private
 
+    _computeSamplingData(startTime, endTime)
+    {
+        // Compute per-millisecond samples of what the main thread was doing.
+        // We construct an array for every millisecond between the start and end time
+        // and mark each millisecond with the best representation of the work that
+        // was being done at that time. We start by populating the samples with
+        // all of the script periods and then override with layout and rendering
+        // samples. This means a forced layout would be counted as a layout:
+        //
+        // Initial:        [ ------, ------, ------, ------, ------ ]
+        // Script Samples: [ ------, Script, Script, Script, ------ ]
+        // Layout Samples: [ ------, Script, Layout, Script, ------ ]
+        //
+        // The undefined samples are considered Idle, but in actuality WebKit
+        // may have been doing some work (such as hit testing / inspector protocol)
+        // that is not included it in generic Timeline data. This just works with
+        // with the data available to the frontend and is quite accurate for most
+        // Main Thread activity.
+
+        const includeRecordBeforeStart = true;
+
+        let scriptTimeline = this._recording.timelineForRecordType(WI.TimelineRecord.Type.Script);
+        let scriptRecords = scriptTimeline ? scriptTimeline.recordsInTimeRange(startTime, endTime, includeRecordBeforeStart) : [];
+        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:
+                // These event types define script entry/exits.
+                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:
+                // These event types have no time range, or are contained by the others.
+                return false;
+
+            default:
+                console.error("Unhandled ScriptTimelineRecord.EventType", record.eventType);
+                return false;
+            }
+        });
+
+        let layoutTimeline = this._recording.timelineForRecordType(WI.TimelineRecord.Type.Layout);
+        let layoutRecords = layoutTimeline ? layoutTimeline.recordsInTimeRange(startTime, endTime, includeRecordBeforeStart) : [];
+        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:
+                // These event types define layout and rendering entry/exits.
+                return true;
+
+            case WI.LayoutTimelineRecord.EventType.InvalidateStyles:
+            case WI.LayoutTimelineRecord.EventType.InvalidateLayout:
+                // These event types have no time range.
+                return false;
+
+            default:
+                console.error("Unhandled LayoutTimelineRecord.EventType", record.eventType);
+                return false;
+            }
+        });
+
+        let millisecondStartTime = Math.round(startTime * 1000);
+        let millisecondEndTime = Math.round(endTime * 1000);
+        let millisecondDuration = millisecondEndTime - millisecondStartTime;
+
+        let samples = new Array(millisecondDuration);
+
+        function markRecordEntries(records, callback) {
+            for (let record of records) {
+                let recordStart = Math.round(record.startTime * 1000);
+                let recordEnd = Math.round(record.endTime * 1000);
+                if (recordStart > millisecondEndTime)
+                    continue;
+                if (recordEnd < millisecondStartTime)
+                    continue;
+
+                let offset = recordStart - millisecondStartTime;
+                recordStart = Math.max(recordStart, millisecondStartTime);
+                recordEnd = Math.min(recordEnd, millisecondEndTime);
+
+                let value = callback(record);
+                for (let t = recordStart; t <= recordEnd; ++t)
+                    samples[t - millisecondStartTime] = value;
+            }
+        }
+
+        markRecordEntries(scriptRecords, (record) => {
+            return WI.CPUTimelineView.SampleType.Script;
+        });
+
+        markRecordEntries(layoutRecords, (record) => {
+            switch (record.eventType) {
+            case WI.LayoutTimelineRecord.EventType.RecalculateStyles:
+                return WI.CPUTimelineView.SampleType.Style;
+            case WI.LayoutTimelineRecord.EventType.ForcedLayout:
+            case WI.LayoutTimelineRecord.EventType.Layout:
+                return WI.CPUTimelineView.SampleType.Layout;
+            case WI.LayoutTimelineRecord.EventType.Paint:
+            case WI.LayoutTimelineRecord.EventType.Composite:
+                return WI.CPUTimelineView.SampleType.Paint;
+            }
+        });
+
+        let samplesIdle = 0;
+        let samplesScript = 0;
+        let samplesLayout = 0;
+        let samplesPaint = 0;
+        let samplesStyle = 0;
+        for (let i = 0; i < samples.length; ++i) {
+            switch (samples[i]) {
+            case undefined:
+                samplesIdle++;
+                break;
+            case WI.CPUTimelineView.SampleType.Script:
+                samplesScript++;
+                break;
+            case WI.CPUTimelineView.SampleType.Layout:
+                samplesLayout++;
+                break;
+            case WI.CPUTimelineView.SampleType.Paint:
+                samplesPaint++;
+                break;
+            case WI.CPUTimelineView.SampleType.Style:
+                samplesStyle++;
+                break;
+            }
+        }
+
+        return {
+            samples,
+            samplesIdle,
+            samplesScript,
+            samplesLayout,
+            samplesPaint,
+            samplesStyle,
+        };
+    }
+
+    _removeWorkerThreadViews()
+    {
+        if (!this._workerViews.length)
+            return;
+
+        for (let view of this._workerViews)
+            this.removeSubview(view);
+
+        this._workerViews = [];
+    }
+
+    _clearBreakdownLegend()
+    {
+        this._breakdownLegendScriptElement.textContent = emDash;
+        this._breakdownLegendLayoutElement.textContent = emDash;
+        this._breakdownLegendPaintElement.textContent = emDash;
+        this._breakdownLegendStyleElement.textContent = emDash;
+
+        this._breakdownChart.centerElement.removeChildren();
+    }
+
     _cpuTimelineRecordAdded(event)
     {
         let cpuTimelineRecord = event.data.record;
@@ -196,3 +700,11 @@ WI.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
             this.needsLayout();
     }
 };
+
+// NOTE: UI follows this order.
+WI.CPUTimelineView.SampleType = {
+    Script: "sample-type-script",
+    Layout: "sample-type-layout",
+    Paint: "sample-type-paint",
+    Style: "sample-type-style",
+};
diff --git a/Source/WebInspectorUI/UserInterface/Views/CPUUsageStackedView.css b/Source/WebInspectorUI/UserInterface/Views/CPUUsageStackedView.css
new file mode 100644 (file)
index 0000000..ee07bdf
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * 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-stacked-view {
+    display: flex;
+    width: 100%;
+    height: calc(var(--cpu-usage-stacked-view-height) + 1px); /* +1 for border-bottom */
+    border-bottom: 1px solid var(--border-color);
+}
+
+.cpu-usage-stacked-view > .details {
+    flex-shrink: 0;
+    width: 150px;
+    padding-top: 10px;
+    -webkit-padding-start: 15px;
+    font-family: -webkit-system-font, sans-serif;
+    font-size: 12px;
+    color: var(--text-color-secondary);
+    overflow: hidden;
+    text-overflow: ellipsis;
+    -webkit-border-end: 1px solid var(--border-color);
+}
+
+.cpu-usage-stacked-view > .details > .name {
+    color: var(--text-color);
+    white-space: nowrap;
+}
+
+body[dir=rtl] .cpu-usage-stacked-view > .graph {
+    transform: scaleX(-1);
+}
+
+.cpu-usage-stacked-view > .graph {
+    position: relative;
+}
+
+.cpu-usage-stacked-view > .graph,
+.cpu-usage-stacked-view > .graph > .stacked-line-chart,
+.cpu-usage-stacked-view > .graph > .stacked-line-chart > svg {
+    width: 100%;
+    height: 100%;
+}
diff --git a/Source/WebInspectorUI/UserInterface/Views/CPUUsageStackedView.js b/Source/WebInspectorUI/UserInterface/Views/CPUUsageStackedView.js
new file mode 100644 (file)
index 0000000..a170f0d
--- /dev/null
@@ -0,0 +1,134 @@
+/*
+ * 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.CPUUsageStackedView = class CPUUsageStackedView extends WI.View
+{
+    constructor(displayName)
+    {
+        super();
+
+        this.element.classList.add("cpu-usage-stacked-view");
+
+        this._detailsElement = this.element.appendChild(document.createElement("div"));
+        this._detailsElement.classList.add("details");
+
+        let detailsNameElement = this._detailsElement.appendChild(document.createElement("span"));
+        detailsNameElement.classList.add("name");
+        detailsNameElement.textContent = displayName;
+
+        this._detailsElement.appendChild(document.createElement("br"));
+        this._detailsAverageElement = this._detailsElement.appendChild(document.createElement("span"));
+        this._detailsElement.appendChild(document.createElement("br"));
+        this._detailsMaxElement = this._detailsElement.appendChild(document.createElement("span"));
+        this._detailsElement.appendChild(document.createElement("br"));
+        this._detailsMinElement = this._detailsElement.appendChild(document.createElement("span"));
+        this._updateDetails(NaN, NaN);
+
+        this._graphElement = this.element.appendChild(document.createElement("div"));
+        this._graphElement.classList.add("graph");
+
+        this._chart = new WI.StackedLineChart;
+        this._chart.initializeSections(["main-thread-usage", "worker-thread-usage", "total-usage"]);
+        this.addSubview(this._chart);
+        this._graphElement.appendChild(this._chart.element);
+    }
+
+    // Public
+
+    get chart() { return this._chart; }
+
+    clear()
+    {
+        this._cachedAverageSize = undefined;
+        this._cachedMinSize = undefined;
+        this._cachedMaxSize = undefined;
+        this._updateDetails(NaN, NaN);
+
+        this._chart.clear();
+        this._chart.needsLayout();
+    }
+
+    updateChart(dataPoints, size, visibleEndTime, min, max, average, xScale, yScale)
+    {
+        console.assert(size instanceof WI.Size);
+        console.assert(min >= 0);
+        console.assert(max >= 0);
+        console.assert(min <= max);
+        console.assert(min <= average && average <= max);
+
+        this._updateDetails(min, max, average);
+
+        this._chart.clear();
+        this._chart.size = size;
+        this._chart.needsLayout();
+
+        if (!dataPoints.length)
+            return;
+
+        // Ensure an empty graph is empty.
+        if (!max)
+            return;
+
+        // Extend the first data point to the start so it doesn't look like we originate at zero size.
+        let firstX = 0;
+        let firstY1 = yScale(dataPoints[0].mainThreadUsage);
+        let firstY2 = yScale(dataPoints[0].mainThreadUsage + dataPoints[0].workerThreadUsage);
+        let firstY3 = yScale(dataPoints[0].usage);
+        this._chart.addPointSet(firstX, [firstY1, firstY2, firstY3]);
+
+        // Points for data points.
+        for (let dataPoint of dataPoints) {
+            let x = xScale(dataPoint.time);
+            let y1 = yScale(dataPoint.mainThreadUsage);
+            let y2 = yScale(dataPoint.mainThreadUsage + dataPoint.workerThreadUsage);
+            let y3 = yScale(dataPoint.usage)
+            this._chart.addPointSet(x, [y1, y2, y3]);
+        }
+
+        // Extend the last data point to the end time.
+        let lastDataPoint = dataPoints.lastValue;
+        let lastX = Math.floor(xScale(visibleEndTime));
+        let lastY1 = yScale(lastDataPoint.mainThreadUsage);
+        let lastY2 = yScale(lastDataPoint.mainThreadUsage + lastDataPoint.workerThreadUsage);
+        let lastY3 = yScale(lastDataPoint.usage);
+        this._chart.addPointSet(lastX, [lastY1, lastY2, lastY3]);
+    }
+
+    // Private
+
+    _updateDetails(minSize, maxSize, averageSize)
+    {
+        if (this._cachedMinSize === minSize && this._cachedMaxSize === maxSize && this._cachedAverageSize === averageSize)
+            return;
+
+        this._cachedAverageSize = averageSize;
+        this._cachedMinSize = minSize;
+        this._cachedMaxSize = maxSize;
+
+        this._detailsAverageElement.textContent = WI.UIString("Average: %s").format(Number.isFinite(maxSize) ? Number.percentageString(averageSize / 100) : emDash);
+        this._detailsMaxElement.textContent = WI.UIString("Highest: %s").format(Number.isFinite(maxSize) ? Number.percentageString(maxSize / 100) : emDash);
+        this._detailsMinElement.textContent = WI.UIString("Lowest: %s").format(Number.isFinite(minSize) ? Number.percentageString(minSize / 100) : emDash);
+    }
+};
index ad47598..91b10f6 100644 (file)
@@ -26,7 +26,7 @@
 .cpu-usage-view {
     display: flex;
     width: 100%;
-    height: 76px; /* Keep this in sync with cpuUsageViewHeight + 1 (for border-bottom) */
+    height: calc(var(--cpu-usage-view-height) + 1px); /* +1 for border-bottom */
     border-bottom: 1px solid var(--border-color);
 }
 
     flex-shrink: 0;
     width: 150px;
     padding-top: 10px;
+    -webkit-padding-start: 15px;
     font-family: -webkit-system-font, sans-serif;
     font-size: 12px;
     color: var(--text-color-secondary);
-    -webkit-padding-start: 15px;
-
-    --cpu-usage-view-details-border-end: 1px solid var(--border-color);
+    overflow: hidden;
+    text-overflow: ellipsis;
+    -webkit-border-end: 1px solid var(--border-color);
 }
 
-body[dir=ltr] .cpu-usage-view > .details {
-    border-right: var(--cpu-usage-view-details-border-end);
-}
-
-body[dir=rtl] .cpu-usage-view > .details {
-    border-left: var(--cpu-usage-view-details-border-end);
+.cpu-usage-view > .details > .name {
+    color: var(--text-color);
+    white-space: nowrap;
 }
 
 body[dir=rtl] .cpu-usage-view > .graph {
     transform: scaleX(-1);
 }
 
+.cpu-usage-view > .graph {
+    position: relative;
+}
+
 .cpu-usage-view > .graph,
 .cpu-usage-view > .graph > .line-chart,
 .cpu-usage-view > .graph > .line-chart > svg {
index c23aa2b..80988d2 100644 (file)
@@ -25,7 +25,7 @@
 
 WI.CPUUsageView = class CPUUsageView extends WI.View
 {
-    constructor()
+    constructor(displayName)
     {
         super();
 
@@ -34,11 +34,16 @@ WI.CPUUsageView = class CPUUsageView extends WI.View
         this._detailsElement = this.element.appendChild(document.createElement("div"));
         this._detailsElement.classList.add("details");
 
+        if (displayName) {
+            let detailsNameElement = this._detailsElement.appendChild(document.createElement("span"));
+            detailsNameElement.classList.add("name");
+            detailsNameElement.textContent = displayName;
+            this._detailsElement.appendChild(document.createElement("br"));
+        }
+
         this._detailsAverageElement = this._detailsElement.appendChild(document.createElement("span"));
         this._detailsElement.appendChild(document.createElement("br"));
         this._detailsMaxElement = this._detailsElement.appendChild(document.createElement("span"));
-        this._detailsElement.appendChild(document.createElement("br"));
-        this._detailsMinElement = this._detailsElement.appendChild(document.createElement("span"));
         this._updateDetails(NaN, NaN);
 
         this._graphElement = this.element.appendChild(document.createElement("div"));
@@ -51,23 +56,25 @@ WI.CPUUsageView = class CPUUsageView extends WI.View
 
     // Public
 
+    get chart() { return this._chart; }
+
     clear()
     {
         this._cachedAverageSize = undefined;
-        this._cachedMinSize = undefined;
         this._cachedMaxSize = undefined;
         this._updateDetails(NaN, NaN);
 
         this._chart.clear();
     }
 
-    updateChart(dataPoints, size, visibleEndTime, min, max, average, xScale, yScale)
+    updateChart(dataPoints, size, visibleEndTime, min, max, average, xScale, yScale, property)
     {
         console.assert(size instanceof WI.Size);
         console.assert(min >= 0);
         console.assert(max >= 0);
         console.assert(min <= max);
         console.assert(min <= average && average <= max);
+        console.assert(property, "CPUUsageView needs a property of the dataPoints to graph");
 
         this._updateDetails(min, max, average);
 
@@ -84,20 +91,20 @@ WI.CPUUsageView = class CPUUsageView extends WI.View
 
         // Extend the first data point to the start so it doesn't look like we originate at zero size.
         let firstX = 0;
-        let firstY = yScale(dataPoints[0].size);
+        let firstY = yScale(dataPoints[0][property]);
         this._chart.addPoint(firstX, firstY);
 
         // Points for data points.
         for (let dataPoint of dataPoints) {
             let x = xScale(dataPoint.time);
-            let y = yScale(dataPoint.size);
+            let y = yScale(dataPoint[property]);
             this._chart.addPoint(x, y);
         }
 
         // Extend the last data point to the end time.
         let lastDataPoint = dataPoints.lastValue;
         let lastX = Math.floor(xScale(visibleEndTime));
-        let lastY = yScale(lastDataPoint.size);
+        let lastY = yScale(lastDataPoint[property]);
         this._chart.addPoint(lastX, lastY);
     }
 
@@ -105,15 +112,16 @@ WI.CPUUsageView = class CPUUsageView extends WI.View
 
     _updateDetails(minSize, maxSize, averageSize)
     {
-        if (this._cachedMinSize === minSize && this._cachedMaxSize === maxSize && this._cachedAverageSize === averageSize)
+        if (this._cachedMaxSize === maxSize && this._cachedAverageSize === averageSize)
             return;
 
         this._cachedAverageSize = averageSize;
-        this._cachedMinSize = minSize;
         this._cachedMaxSize = maxSize;
 
-        this._detailsAverageElement.textContent = WI.UIString("Average: %s").format(Number.isFinite(maxSize) ? Number.percentageString(averageSize / 100) : emDash);
+        this._detailsAverageElement.hidden = !Number.isFinite(averageSize);
+        this._detailsMaxElement.hidden = !Number.isFinite(maxSize);
+
+        this._detailsAverageElement.textContent = WI.UIString("Average: %s").format(Number.isFinite(averageSize) ? Number.percentageString(averageSize / 100) : emDash);
         this._detailsMaxElement.textContent = WI.UIString("Highest: %s").format(Number.isFinite(maxSize) ? Number.percentageString(maxSize / 100) : emDash);
-        this._detailsMinElement.textContent = WI.UIString("Lowest: %s").format(Number.isFinite(minSize) ? Number.percentageString(minSize / 100) : emDash);
     }
 };
index a37d9a4..48ad3af 100644 (file)
@@ -65,5 +65,5 @@ body[dir=rtl] .timeline-view.legacy-cpu > .content > .details > .timeline-ruler
 
 .timeline-view.legacy-cpu .cpu-usage-view .line-chart > svg > path {
     stroke: var(--cpu-stroke-color);
-    fill: var(--cpu-fill-color);
+    fill: var(--cpu-main-thread-fill-color);
 }
index 86b4617..1091e7d 100644 (file)
@@ -57,6 +57,10 @@ WI.LegacyCPUTimelineView = class LegacyCPUTimelineView extends WI.TimelineView
         timeline.addEventListener(WI.Timeline.Event.RecordAdded, this._cpuTimelineRecordAdded, this);
     }
 
+    // Static
+
+    static get cpuUsageViewHeight() { return 150; }
+
     // Public
 
     shown()
@@ -95,6 +99,11 @@ WI.LegacyCPUTimelineView = class LegacyCPUTimelineView extends WI.TimelineView
 
     get showsFilterBar() { return false; }
 
+    initialLayout()
+    {
+        this.element.style.setProperty("--cpu-usage-view-height", LegacyCPUTimelineView.cpuUsageViewHeight + "px");
+    }
+
     layout()
     {
         if (this.layoutReason === WI.View.LayoutReason.Resize)
@@ -105,8 +114,6 @@ WI.LegacyCPUTimelineView = class LegacyCPUTimelineView extends WI.TimelineView
         this._timelineRuler.startTime = this.startTime;
         this._timelineRuler.endTime = this.endTime;
 
-        const cpuUsageViewHeight = 75; // Keep this in sync with .legacy-cpu-usage-view
-
         let graphStartTime = this.startTime;
         let graphEndTime = this.endTime;
         let secondsPerPixel = this._timelineRuler.secondsPerPixel;
@@ -168,13 +175,13 @@ WI.LegacyCPUTimelineView = class LegacyCPUTimelineView extends WI.TimelineView
                 return (time - graphStartTime) / secondsPerPixel;
             }
 
-            let size = new WI.Size(xScale(graphEndTime), cpuUsageViewHeight);
+            let size = new WI.Size(xScale(graphEndTime), LegacyCPUTimelineView.cpuUsageViewHeight);
 
             function yScale(value) {
                 return size.height - (((value - graphMin) / graphMax) * size.height);
             }
 
-            view.updateChart(dataPoints, size, visibleEndTime, min, max, average, xScale, yScale);
+            view.updateChart(dataPoints, size, visibleEndTime, min, max, average, xScale, yScale, "size");
         }
 
         layoutView(this._cpuUsageView, {dataPoints, min, max, average});
index d5fd3d9..36c0de8 100644 (file)
     flex-shrink: 0;
     width: 150px;
     padding-top: 10px;
+    -webkit-padding-start: 15px;
     font-family: -webkit-system-font, sans-serif;
     font-size: 12px;
     color: var(--text-color-secondary);
-    -webkit-padding-start: 15px;
-
-    --memory-category-view-details-border-end: 1px solid var(--border-color);
-}
-
-body[dir=ltr] .memory-category-view > .details {
-    border-right: var(--memory-category-view-details-border-end);
-}
-
-body[dir=rtl] .memory-category-view > .details {
-    border-left: var(--memory-category-view-details-border-end);
+    overflow: hidden;
+    text-overflow: ellipsis;
+    -webkit-border-end: 1px solid var(--border-color);
 }
 
 .memory-category-view > .details > .name {
     color: var(--text-color);
+    white-space: nowrap;
 }
 
-
 body[dir=rtl] .memory-category-view > .graph {
     transform: scaleX(-1);
 }
index 17e7431..498270d 100644 (file)
@@ -210,8 +210,6 @@ WI.MemoryTimelineOverviewGraph = class MemoryTimelineOverviewGraph extends WI.Ti
                 this._chart.addPointSet(x, pointSetForRecord(lastRecord));
             }
         }
-
-        this._chart.updateLayout();
     }
 
     // Private
index 060cf33..48f5906 100644 (file)
@@ -23,7 +23,7 @@
  * THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-.timeline-view.memory {
+body .timeline-view.memory {
     overflow: scroll;
 }
 
diff --git a/Source/WebInspectorUI/UserInterface/Views/StackedColumnChart.js b/Source/WebInspectorUI/UserInterface/Views/StackedColumnChart.js
new file mode 100644 (file)
index 0000000..250b8a6
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * 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.
+ */
+
+// StackedColumnChart creates a chart with filled columns each stratified with sections.
+//
+// Initialize the chart with a size.
+// To populate with data, first initialize the sections. The class names you
+// provide for the segments will allow you to style them. You can then include
+// a new set of (x, totalHeight, w, [h1, h2, h3]) points in the chart via `addColumnSet`.
+// The order of `h` values must be in the same order as the sections.
+// The `y` value to be used for each column is `totalHeight - h`.
+//
+// SVG:
+//
+// - There is a single rect for each bar and each section.
+// - Each bar extends all the way down to the bottom, they are layered such
+//   that the rects for early sections overlap the sections for later sections.
+//
+//  <div class="stacked-column-chart">
+//      <svg viewBox="0 0 800 75">
+//          <rect class="section-class-name-3" width="<w>" height="<h3>" transform="translate(<x>, <y>)" />
+//          <rect class="section-class-name-2" width="<w>" height="<h2>" transform="translate(<x>, <y>)" />
+//          <rect class="section-class-name-1" width="<w>" height="<h1>" transform="translate(<x>, <y>)" />
+//          ...
+//      </svg>
+//  </div>
+
+WI.StackedColumnChart = class StackedColumnChart extends WI.View
+{
+    constructor(size)
+    {
+        super();
+
+        this.element.classList.add("stacked-column-chart");
+
+        this._svgElement = this.element.appendChild(createSVGElement("svg"));
+        this._svgElement.setAttribute("preserveAspectRatio", "none");
+
+        this._sections = null;
+        this._columns = [];
+        this.size = size;
+    }
+
+    // Public
+
+    get size()
+    {
+        return this._size;
+    }
+
+    set size(size)
+    {
+        this._size = size;
+
+        this._svgElement.setAttribute("viewBox", `0 0 ${size.width} ${size.height}`);
+    }
+
+    initializeSections(sectionClassNames)
+    {
+        console.assert(this._sections === null, "Should not initialize multiple times");
+
+        this._sections = sectionClassNames;
+    }
+
+    addColumnSet(x, totalHeight, width, heights)
+    {
+        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});
+    }
+
+    clear()
+    {
+        this._columns = [];
+    }
+
+    // Protected
+
+    layout()
+    {
+        super.layout();
+
+        if (this.layoutReason === WI.View.LayoutReason.Resize)
+            return;
+
+        this._svgElement.removeChildren();
+
+        for (let {x, totalHeight, width, heights} of this._columns) {
+            let sectionIndex = 0;
+            for (let i = heights.length - 1; i >= 0; --i) {
+                let height = heights[i];
+                // Next rect will be identical, skip this one.
+                if (height === heights[i - 1])
+                    continue;
+                let y = totalHeight - height;
+                let rect = this._svgElement.appendChild(createSVGElement("rect"));
+                rect.classList.add(this._sections[i]);
+                rect.setAttribute("width", width);
+                rect.setAttribute("height", height);
+                rect.setAttribute("transform", `translate(${x}, ${y})`);
+            }
+        }
+    }
+};
index 37774a4..cc9d35c 100644 (file)
     --memory-max-comparison-stroke-color: hsl(220, 10%, 55%);
 
     --cpu-stroke-color: hsl(118, 33%, 42%);
-    --cpu-fill-color: hsl(118, 43%, 55%);
+    --cpu-fill-color: hsl(81, 80%, 50%);
+    --cpu-main-thread-fill-color: hsl(118, 43%, 55%);
+    --cpu-worker-thread-fill-color: hsl(45, 94.75%, 55%);
+
+    --cpu-idle-fill-color: hsl(220, 10%, 75%);
+    --cpu-idle-stroke-color: hsl(220, 10%, 55%);
+    --cpu-script-fill-color: hsl(269, 65%, 75%);
+    --cpu-script-stroke-color: hsl(269, 33%, 50%);
+    --cpu-style-fill-color: hsl(22, 60%, 70%);
+    --cpu-style-stroke-color: hsl(22, 40%, 50%);
+    --cpu-layout-fill-color: hsl(0, 65%, 75%);
+    --cpu-layout-stroke-color: hsl(0, 54%, 50%);
+    --cpu-paint-fill-color: hsl(76, 49%, 75%);
+    --cpu-paint-stroke-color: hsl(79, 45%, 50%);
 
     --network-header-color: hsl(204, 52%, 55%);
     --network-system-color: hsl(79, 32%, 50%);
index 39bdd8b..4f2591f 100644 (file)
@@ -117,7 +117,7 @@ WI.View = class View extends WI.Object
     removeSubview(view)
     {
         console.assert(view instanceof WI.View);
-        console.assert(view.element.parentNode === this._element, "Subview DOM element must be a child of the parent view element.");
+        console.assert(this._element.contains(view.element), "Subview DOM element must be a child of the parent view element.");
 
         let index = this._subviews.lastIndexOf(view);
         if (index === -1) {