Web Inspector: Timeline graphs have drawing issues with multiple discontinuities
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / CPUTimelineView.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.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
27 {
28     constructor(timeline, extraArguments)
29     {
30         console.assert(timeline.type === WI.TimelineRecord.Type.CPU, timeline);
31
32         super(timeline, extraArguments);
33
34         this._recording = extraArguments.recording;
35         this._maxUsage = -Infinity;
36
37         this.element.classList.add("cpu");
38
39         let contentElement = this.element.appendChild(document.createElement("div"));
40         contentElement.classList.add("content");
41
42         // FIXME: Overview with charts.
43
44         let detailsContainerElement = this._detailsContainerElement = contentElement.appendChild(document.createElement("div"));
45         detailsContainerElement.classList.add("details");
46
47         this._timelineRuler = new WI.TimelineRuler;
48         this.addSubview(this._timelineRuler);
49         detailsContainerElement.appendChild(this._timelineRuler.element);
50
51         let detailsSubtitleElement = detailsContainerElement.appendChild(document.createElement("div"));
52         detailsSubtitleElement.classList.add("subtitle");
53         detailsSubtitleElement.textContent = WI.UIString("CPU Usage");
54
55         this._cpuUsageView = new WI.CPUUsageView;
56         this.addSubview(this._cpuUsageView);
57         this._detailsContainerElement.appendChild(this._cpuUsageView.element);
58
59         timeline.addEventListener(WI.Timeline.Event.RecordAdded, this._cpuTimelineRecordAdded, this);
60     }
61
62     // Public
63
64     shown()
65     {
66         super.shown();
67
68         this._timelineRuler.updateLayout(WI.View.LayoutReason.Resize);
69     }
70
71     closed()
72     {
73         console.assert(this.representedObject instanceof WI.Timeline);
74         this.representedObject.removeEventListener(null, null, this);
75     }
76
77     reset()
78     {
79         super.reset();
80
81         this._maxUsage = -Infinity;
82
83         this._cpuUsageView.clear();
84     }
85
86     get scrollableElements()
87     {
88         return [this.element];
89     }
90
91     // Protected
92
93     get showsFilterBar() { return false; }
94
95     layout()
96     {
97         if (this.layoutReason === WI.View.LayoutReason.Resize)
98             return;
99
100         // Always update timeline ruler.
101         this._timelineRuler.zeroTime = this.zeroTime;
102         this._timelineRuler.startTime = this.startTime;
103         this._timelineRuler.endTime = this.endTime;
104
105         const cpuUsageViewHeight = 75; // Keep this in sync with .cpu-usage-view
106
107         let graphStartTime = this.startTime;
108         let graphEndTime = this.endTime;
109         let secondsPerPixel = this._timelineRuler.secondsPerPixel;
110         let visibleEndTime = Math.min(this.endTime, this.currentTime);
111
112         let discontinuities = this._recording.discontinuitiesInTimeRange(graphStartTime, visibleEndTime);
113
114         // Don't include the record before the graph start if the graph start is within a gap.
115         let includeRecordBeforeStart = !discontinuities.length || discontinuities[0].startTime > graphStartTime;
116         let visibleRecords = this.representedObject.recordsInTimeRange(graphStartTime, visibleEndTime, includeRecordBeforeStart);
117         if (!visibleRecords.length)
118             return;
119
120         // Update total usage chart with the last record's data.
121         let lastRecord = visibleRecords.lastValue;
122
123         // FIXME: Left chart.
124         // FIXME: Right chart.
125
126         let dataPoints = [];
127         let max = -Infinity;
128         let min = Infinity;
129         let average = 0;
130
131         for (let record of visibleRecords) {
132             let time = record.startTime;
133             let usage = record.usage;
134
135             if (discontinuities.length && discontinuities[0].endTime < time) {
136                 let startDiscontinuity = discontinuities.shift();
137                 let endDiscontinuity = startDiscontinuity;
138                 while (discontinuities.length && discontinuities[0].endTime < time)
139                     endDiscontinuity = discontinuities.shift();
140                 dataPoints.push({time: startDiscontinuity.startTime, size: 0});
141                 dataPoints.push({time: endDiscontinuity.endTime, size: 0});
142                 dataPoints.push({time: endDiscontinuity.endTime, size: usage});
143             }
144
145             dataPoints.push({time, size: usage});
146             max = Math.max(max, usage);
147             min = Math.min(min, usage);
148             average += usage;
149         }
150
151         average /= visibleRecords.length;
152
153         // If the graph end time is inside a gap, the last data point should
154         // only be extended to the start of the discontinuity.
155         if (discontinuities.length)
156             visibleEndTime = discontinuities[0].startTime;
157
158         function layoutView(view, {dataPoints, min, max, average}) {
159             if (min === Infinity)
160                 min = 0;
161             if (max === -Infinity)
162                 max = 0;
163
164             // Zoom in to the top of each graph to accentuate small changes.
165             let graphMin = min * 0.95;
166             let graphMax = (max * 1.05) - graphMin;
167
168             function xScale(time) {
169                 return (time - graphStartTime) / secondsPerPixel;
170             }
171
172             let size = new WI.Size(xScale(graphEndTime), cpuUsageViewHeight);
173
174             function yScale(value) {
175                 return size.height - (((value - graphMin) / graphMax) * size.height);
176             }
177
178             view.updateChart(dataPoints, size, visibleEndTime, min, max, average, xScale, yScale);
179         }
180
181         layoutView(this._cpuUsageView, {dataPoints, min, max, average});
182     }
183
184     // Private
185
186     _cpuTimelineRecordAdded(event)
187     {
188         let cpuTimelineRecord = event.data.record;
189         console.assert(cpuTimelineRecord instanceof WI.CPUTimelineRecord);
190
191         this._maxUsage = Math.max(this._maxUsage, cpuTimelineRecord.usage);
192
193         if (cpuTimelineRecord.startTime >= this.startTime && cpuTimelineRecord.endTime <= this.endTime)
194             this.needsLayout();
195     }
196 };