Web Inspector: CPU Usage Timeline - Add legend and graph hover effects
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / CPUUsageCombinedView.js
1 /*
2  * Copyright (C) 2019 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WI.CPUUsageCombinedView = class CPUUsageCombinedView extends WI.View
27 {
28     constructor(displayName)
29     {
30         super();
31
32         this.element.classList.add("cpu-usage-combined-view");
33
34         this._detailsElement = this.element.appendChild(document.createElement("div"));
35         this._detailsElement.classList.add("details");
36
37         let detailsNameElement = this._detailsElement.appendChild(document.createElement("span"));
38         detailsNameElement.classList.add("name");
39         detailsNameElement.textContent = displayName;
40
41         this._detailsElement.appendChild(document.createElement("br"));
42         this._detailsAverageElement = this._detailsElement.appendChild(document.createElement("span"));
43         this._detailsElement.appendChild(document.createElement("br"));
44         this._detailsMaxElement = this._detailsElement.appendChild(document.createElement("span"));
45         this._detailsElement.appendChild(document.createElement("br"));
46         this._detailsElement.appendChild(document.createElement("br"));
47         this._updateDetails(NaN, NaN);
48
49         this._graphElement = this.element.appendChild(document.createElement("div"));
50         this._graphElement.classList.add("graph");
51
52         // Combined thread usage area chart.
53         this._chart = new WI.StackedAreaChart;
54         this._chart.initializeSections(["main-thread-usage", "worker-thread-usage", "total-usage"]);
55         this.addSubview(this._chart);
56         this._graphElement.appendChild(this._chart.element);
57
58         // Main thread indicator strip.
59         this._rangeChart = new WI.RangeChart;
60         this.addSubview(this._rangeChart);
61         this._graphElement.appendChild(this._rangeChart.element);
62
63         function appendLegendRow(legendElement, className) {
64             let rowElement = legendElement.appendChild(document.createElement("div"));
65             rowElement.classList.add("row");
66
67             let swatchElement = rowElement.appendChild(document.createElement("div"));
68             swatchElement.classList.add("swatch", className);
69
70             let labelElement = rowElement.appendChild(document.createElement("div"));
71             labelElement.classList.add("label");
72
73             return labelElement;
74         }
75
76         this._legendElement = this._detailsElement.appendChild(document.createElement("div"));
77         this._legendElement.classList.add("legend-container");
78
79         this._legendMainThreadElement = appendLegendRow(this._legendElement, "main-thread");
80         this._legendWorkerThreadsElement = appendLegendRow(this._legendElement, "worker-threads");
81         this._legendOtherThreadsElement = appendLegendRow(this._legendElement, "other-threads");
82         this._legendTotalThreadsElement = appendLegendRow(this._legendElement, "total");
83
84         this.clearLegend();
85     }
86
87     // Public
88
89     get graphElement() { return this._graphElement; }
90     get chart() { return this._chart; }
91     get rangeChart() { return this._rangeChart; }
92
93     clear()
94     {
95         this._cachedAverageSize = undefined;
96         this._cachedMaxSize = undefined;
97         this._updateDetails(NaN, NaN);
98
99         this.clearLegend();
100
101         this._chart.clear();
102         this._chart.needsLayout();
103
104         this._rangeChart.clear();
105         this._rangeChart.needsLayout();
106     }
107
108     updateChart(dataPoints, size, visibleEndTime, min, max, average, xScale, yScale)
109     {
110         console.assert(size instanceof WI.Size);
111         console.assert(min >= 0);
112         console.assert(max >= 0);
113         console.assert(min <= max);
114         console.assert(min <= average && average <= max);
115
116         this._updateDetails(max, average);
117
118         this._chart.clearPoints();
119         this._chart.size = size;
120         this._chart.needsLayout();
121
122         if (!dataPoints.length)
123             return;
124
125         // Ensure an empty graph is empty.
126         if (!max)
127             return;
128
129         // Extend the first data point to the start so it doesn't look like we originate at zero size.
130         let firstX = 0;
131         let firstY1 = yScale(dataPoints[0].mainThreadUsage);
132         let firstY2 = yScale(dataPoints[0].mainThreadUsage + dataPoints[0].workerThreadUsage);
133         let firstY3 = yScale(dataPoints[0].usage);
134         this._chart.addPointSet(firstX, [firstY1, firstY2, firstY3]);
135
136         // Points for data points.
137         for (let dataPoint of dataPoints) {
138             let x = xScale(dataPoint.time);
139             let y1 = yScale(dataPoint.mainThreadUsage);
140             let y2 = yScale(dataPoint.mainThreadUsage + dataPoint.workerThreadUsage);
141             let y3 = yScale(dataPoint.usage)
142             this._chart.addPointSet(x, [y1, y2, y3]);
143         }
144
145         // Extend the last data point to the end time.
146         let lastDataPoint = dataPoints.lastValue;
147         let lastX = Math.floor(xScale(visibleEndTime));
148         let lastY1 = yScale(lastDataPoint.mainThreadUsage);
149         let lastY2 = yScale(lastDataPoint.mainThreadUsage + lastDataPoint.workerThreadUsage);
150         let lastY3 = yScale(lastDataPoint.usage);
151         this._chart.addPointSet(lastX, [lastY1, lastY2, lastY3]);
152     }
153
154     updateMainThreadIndicator(samples, size, visibleEndTime, xScale)
155     {
156         console.assert(size instanceof WI.Size);
157
158         this._rangeChart.clear();
159         this._rangeChart.size = size;
160         this._rangeChart.needsLayout();
161
162         if (!samples.length)
163             return;
164
165         // Coalesce ranges of samples.
166         let ranges = [];
167         let currentRange = null;
168         let currentSampleType = undefined;
169         for (let i = 0; i < samples.length; ++i) {
170             // Back to idle, close any current chunk.
171             let type = samples[i];
172             if (!type) {
173                 if (currentRange) {
174                     ranges.push(currentRange);
175                     currentRange = null;
176                     currentSampleType = undefined;
177                 }
178                 continue;
179             }
180
181             // Expand existing chunk.
182             if (type === currentSampleType) {
183                 currentRange.endIndex = i;
184                 continue;
185             }
186
187             // If type changed, close current chunk.
188             if (currentSampleType) {
189                 ranges.push(currentRange);
190                 currentRange = null;
191                 currentSampleType = undefined;
192             }
193
194             // Start a new chunk.
195             console.assert(!currentRange);
196             console.assert(!currentSampleType);
197             currentRange = {type, startIndex: i, endIndex: i};
198             currentSampleType = type;
199         }
200
201         for (let {type, startIndex, endIndex} of ranges) {
202             let startX = xScale(startIndex);
203             let endX = xScale(endIndex + 1);
204             let width = endX - startX;
205             this._rangeChart.addRange(startX, width, type);
206         }
207     }
208
209     clearLegend()
210     {
211         this._legendMainThreadElement.textContent = WI.UIString("Main Thread");
212         this._legendWorkerThreadsElement.textContent = WI.UIString("Worker Threads");
213         this._legendOtherThreadsElement.textContent = WI.UIString("Other Threads");
214         this._legendTotalThreadsElement.textContent = "";
215     }
216
217     updateLegend(record)
218     {
219         if (!record) {
220             this.clearLegend();
221             return;
222         }
223
224         let {usage, mainThreadUsage, workerThreadUsage, webkitThreadUsage, unknownThreadUsage} = record;
225
226         this._legendMainThreadElement.textContent = WI.UIString("Main: %s").format(Number.percentageString(mainThreadUsage / 100));
227         this._legendWorkerThreadsElement.textContent = WI.UIString("Worker: %s").format(Number.percentageString(workerThreadUsage / 100));
228         this._legendOtherThreadsElement.textContent = WI.UIString("Other: %s").format(Number.percentageString((webkitThreadUsage + unknownThreadUsage) / 100));
229         this._legendTotalThreadsElement.textContent = WI.UIString("Total: %s").format(Number.percentageString(usage / 100));
230     }
231
232     // Private
233
234     _updateDetails(maxSize, averageSize)
235     {
236         if (this._cachedMaxSize === maxSize && this._cachedAverageSize === averageSize)
237             return;
238
239         this._cachedAverageSize = averageSize;
240         this._cachedMaxSize = maxSize;
241
242         this._detailsAverageElement.textContent = WI.UIString("Average: %s").format(Number.isFinite(maxSize) ? Number.percentageString(averageSize / 100) : emDash);
243         this._detailsMaxElement.textContent = WI.UIString("Highest: %s").format(Number.isFinite(maxSize) ? Number.percentageString(maxSize / 100) : emDash);
244     }
245 };
246
247 WI.CPUUsageCombinedView._cachedMainThreadIndicatorFillColor = null;
248 WI.CPUUsageCombinedView._cachedMainThreadIndicatorStrokeColor = null;