ad7ea4d906c8fe72d7f6ad00d461059c35a39261
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / MemoryTimelineOverviewGraph.js
1 /*
2  * Copyright (C) 2016 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 WebInspector.MemoryTimelineOverviewGraph = class MemoryTimelineOverviewGraph extends WebInspector.TimelineOverviewGraph
27 {
28     constructor(timeline, timelineOverview)
29     {
30         super(timelineOverview);
31
32         this.element.classList.add("memory");
33
34         console.assert(timeline instanceof WebInspector.MemoryTimeline);
35
36         this._memoryTimeline = timeline;
37         this._memoryTimeline.addEventListener(WebInspector.Timeline.Event.RecordAdded, this._memoryTimelineRecordAdded, this);
38         this._memoryTimeline.addEventListener(WebInspector.MemoryTimeline.Event.MemoryPressureEventAdded, this._memoryTimelineMemoryPressureEventAdded, this)
39
40         this._didInitializeCategories = false;
41
42         let size = new WebInspector.Size(0, this.height);
43         this._chart = new WebInspector.StackedLineChart(size);
44         this.element.appendChild(this._chart.element);
45
46         this._legendElement = this.element.appendChild(document.createElement("div"));
47         this._legendElement.classList.add("legend");
48
49         this._memoryPressureMarkersContainerElement = this.element.appendChild(document.createElement("div"));
50         this._memoryPressureMarkersContainerElement.classList.add("memory-pressure-markers-container");
51         this._memoryPressureMarkerElements = [];
52
53         this.reset();
54     }
55
56     // Protected
57
58     get height()
59     {
60         return 108;
61     }
62
63     reset()
64     {
65         super.reset();
66
67         this._maxSize = 0;
68         this._cachedMaxSize = undefined;
69
70         this._updateLegend();
71         this._chart.clear();
72         this._chart.needsLayout();
73
74         this._memoryPressureMarkersContainerElement.removeChildren();
75         this._memoryPressureMarkerElements = [];
76     }
77
78     layout()
79     {
80         if (!this.visible)
81             return;
82
83         this._updateLegend();
84         this._chart.clear();
85
86         if (!this._didInitializeCategories)
87             return;
88
89         let graphWidth = this.timelineOverview.scrollContainerWidth;
90         if (isNaN(graphWidth))
91             return;
92
93         if (this._chart.size.width !== graphWidth || this._chart.size.height !== this.height)
94             this._chart.size = new WebInspector.Size(graphWidth, this.height);
95
96         let graphStartTime = this.startTime;
97         let graphEndTime = this.endTime;
98         let graphCurrentTime = this.currentTime;
99         let visibleEndTime = Math.min(this.endTime, this.currentTime);
100
101         let secondsPerPixel = this.timelineOverview.secondsPerPixel;
102         let maxCapacity = this._maxSize * 1.05; // Add 5% for padding.
103
104         function xScale(time) {
105             return (time - graphStartTime) / secondsPerPixel;
106         }
107
108         let height = this.height;
109         function yScale(size) {
110             return height - ((size / maxCapacity) * height);
111         }
112
113         let visibleMemoryPressureEventMarkers = this._visibleMemoryPressureEvents(graphStartTime, visibleEndTime);
114
115         // Reuse existing marker elements.
116         for (let i = 0; i < visibleMemoryPressureEventMarkers.length; ++i) {
117             let markerElement = this._memoryPressureMarkerElements[i];
118             if (!markerElement) {
119                 markerElement = this._memoryPressureMarkersContainerElement.appendChild(document.createElement("div"));
120                 markerElement.classList.add("memory-pressure-event");
121                 this._memoryPressureMarkerElements[i] = markerElement;
122             }
123
124             let memoryPressureEvent = visibleMemoryPressureEventMarkers[i];
125             markerElement.style.left = xScale(memoryPressureEvent.timestamp) + "px";
126         }
127
128         // Remove excess marker elements.
129         let excess = this._memoryPressureMarkerElements.length - visibleMemoryPressureEventMarkers.length;
130         if (excess) {
131             let elementsToRemove = this._memoryPressureMarkerElements.splice(visibleMemoryPressureEventMarkers.length);
132             for (let element of elementsToRemove)
133                 element.remove();
134         }
135
136         let discontinuities = this.timelineOverview.discontinuitiesInTimeRange(graphStartTime, visibleEndTime);
137
138         // Don't include the record before the graph start if the graph start is within a gap.
139         let includeRecordBeforeStart = !discontinuities.length || discontinuities[0].startTime > graphStartTime;
140
141         // FIXME: <https://webkit.org/b/153759> Web Inspector: Memory Timelines should better extend to future data
142         let visibleRecords = this._memoryTimeline.recordsInTimeRange(graphStartTime, visibleEndTime, includeRecordBeforeStart);
143         if (!visibleRecords.length)
144             return;
145
146         function pointSetForRecord(record) {
147             let size = 0;
148             let ys = [];
149             for (let i = 0; i < record.categories.length; ++i) {
150                 size += record.categories[i].size;
151                 ys[i] = yScale(size);
152             }
153             return ys;
154         }
155
156         // Extend the first record to the start so it doesn't look like we originate at zero size.
157         if (visibleRecords[0] === this._memoryTimeline.records[0] && (!discontinuities.length || discontinuities[0].startTime > visibleRecords[0].startTime))
158             this._chart.addPointSet(0, pointSetForRecord(visibleRecords[0]));
159
160         function insertDiscontinuity(previousRecord, discontinuity, nextRecord)
161         {
162             console.assert(previousRecord || nextRecord);
163             if (!(previousRecord || nextRecord))
164                 return;
165
166             let xStart = xScale(discontinuity.startTime);
167             let xEnd = xScale(discontinuity.endTime);
168
169             // Extend the previous record to the start of the discontinuity.
170             if (previousRecord)
171                 this._chart.addPointSet(xStart, pointSetForRecord(previousRecord));
172
173             let zeroValues = Array((previousRecord || nextRecord).categories.length).fill(yScale(0));
174             this._chart.addPointSet(xStart, zeroValues);
175
176             if (nextRecord) {
177                 this._chart.addPointSet(xEnd, zeroValues);
178                 this._chart.addPointSet(xEnd, pointSetForRecord(nextRecord));
179             } else {
180                 // Extend the discontinuity to the visible end time to prevent
181                 // drawing artifacts when the next record arrives.
182                 this._chart.addPointSet(xScale(visibleEndTime), zeroValues);
183             }
184         }
185
186         // Points for visible records.
187         let previousRecord = null;
188         for (let record of visibleRecords) {
189             if (discontinuities.length && discontinuities[0].endTime < record.startTime) {
190                 let discontinuity = discontinuities.shift();
191                 insertDiscontinuity.call(this, previousRecord, discontinuity, record);
192             }
193
194             let x = xScale(record.startTime);
195             this._chart.addPointSet(x, pointSetForRecord(record));
196
197             previousRecord = record;
198         }
199
200         if (discontinuities.length)
201             insertDiscontinuity.call(this, previousRecord, discontinuities[0], null);
202         else {
203             // Extend the last value to current / end time.
204             let lastRecord = visibleRecords.lastValue;
205             if (lastRecord.startTime <= visibleEndTime) {
206                 let x = Math.floor(xScale(visibleEndTime));
207                 this._chart.addPointSet(x, pointSetForRecord(lastRecord));
208             }
209         }
210
211         this._chart.updateLayout();
212     }
213
214     // Private
215
216     _updateLegend()
217     {
218         if (this._cachedMaxSize === this._maxSize)
219             return;
220
221         this._cachedMaxSize = this._maxSize;
222
223         if (!this._maxSize) {
224             this._legendElement.hidden = true;
225             this._legendElement.textContent = "";
226         } else {
227             this._legendElement.hidden = false;
228             this._legendElement.textContent = WebInspector.UIString("Maximum Size: %s").format(Number.bytesToString(this._maxSize));
229         }
230     }
231
232     _visibleMemoryPressureEvents(startTime, endTime)
233     {
234         let events = this._memoryTimeline.memoryPressureEvents;
235         if (!events.length)
236             return [];
237
238         let lowerIndex = events.lowerBound(startTime, (time, event) => time - event.timestamp);
239         let upperIndex = events.upperBound(endTime, (time, event) => time - event.timestamp);
240         return events.slice(lowerIndex, upperIndex);
241     }
242
243     _memoryTimelineRecordAdded(event)
244     {
245         let memoryTimelineRecord = event.data.record;
246
247         this._maxSize = Math.max(this._maxSize, memoryTimelineRecord.totalSize);
248
249         if (!this._didInitializeCategories) {
250             this._didInitializeCategories = true;
251             let types = [];
252             for (let category of memoryTimelineRecord.categories)
253                 types.push(category.type);
254             this._chart.initializeSections(types);
255         }
256
257         this.needsLayout();
258     }
259
260     _memoryTimelineMemoryPressureEventAdded(event)
261     {
262         this.needsLayout();
263     }
264 };