Web Inspector: discontinuous recordings should have discontinuities in the timeline...
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / MemoryTimelineView.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.MemoryTimelineView = class MemoryTimelineView extends WebInspector.TimelineView
27 {
28     constructor(timeline, extraArguments)
29     {
30         super(timeline, extraArguments);
31
32         this._recording = extraArguments.recording;
33
34         console.assert(timeline.type === WebInspector.TimelineRecord.Type.Memory, timeline);
35
36         this.element.classList.add("memory");
37
38         let contentElement = this.element.appendChild(document.createElement("div"));
39         contentElement.classList.add("content");
40
41         let overviewElement = contentElement.appendChild(document.createElement("div"));
42         overviewElement.classList.add("overview");
43
44         function createChartContainer(parentElement, subtitle, tooltip)
45         {
46             let chartElement = parentElement.appendChild(document.createElement("div"));
47             chartElement.classList.add("chart");
48
49             let chartSubtitleElement = chartElement.appendChild(document.createElement("div"));
50             chartSubtitleElement.classList.add("subtitle");
51             chartSubtitleElement.textContent = subtitle;
52             chartSubtitleElement.title = tooltip;
53
54             let chartFlexContainerElement = chartElement.appendChild(document.createElement("div"));
55             chartFlexContainerElement.classList.add("container");
56             return chartFlexContainerElement;
57         }
58
59         let usageTooltip = WebInspector.UIString("Breakdown of each memory category at the end of the selected time range");
60         let usageChartContainerElement = createChartContainer(overviewElement, WebInspector.UIString("Breakdown"), usageTooltip);
61         this._usageCircleChart = new WebInspector.CircleChart({size: 120, innerRadiusRatio: 0.5});
62         usageChartContainerElement.appendChild(this._usageCircleChart.element);
63         this._usageLegendElement = usageChartContainerElement.appendChild(document.createElement("div"));
64         this._usageLegendElement.classList.add("legend", "usage");
65
66         let dividerElement = overviewElement.appendChild(document.createElement("div"));
67         dividerElement.classList.add("divider");
68
69         let maxComparisonTooltip = WebInspector.UIString("Comparison of total memory size at the end of the selected time range to the maximum memory size in this recording");
70         let maxComparisonChartContainerElement = createChartContainer(overviewElement, WebInspector.UIString("Max Comparison"), maxComparisonTooltip);
71         this._maxComparisonCircleChart = new WebInspector.CircleChart({size: 120, innerRadiusRatio: 0.5});
72         maxComparisonChartContainerElement.appendChild(this._maxComparisonCircleChart.element);
73         this._maxComparisonLegendElement = maxComparisonChartContainerElement.appendChild(document.createElement("div"));
74         this._maxComparisonLegendElement.classList.add("legend", "maximum");
75
76         let detailsContainerElement = this._detailsContainerElement = contentElement.appendChild(document.createElement("div"));
77         detailsContainerElement.classList.add("details");
78
79         this._timelineRuler = new WebInspector.TimelineRuler;
80         this.addSubview(this._timelineRuler);
81         detailsContainerElement.appendChild(this._timelineRuler.element);
82
83         let detailsSubtitleElement = detailsContainerElement.appendChild(document.createElement("div"));
84         detailsSubtitleElement.classList.add("subtitle");
85         detailsSubtitleElement.textContent = WebInspector.UIString("Categories");
86
87         this._didInitializeCategories = false;
88         this._categoryViews = [];
89         this._usageLegendSizeElementMap = new Map;
90
91         this._maxSize = 0;
92         this._maxComparisonMaximumSizeElement = null;
93         this._maxComparisonCurrentSizeElement = null;
94
95         timeline.addEventListener(WebInspector.Timeline.Event.RecordAdded, this._memoryTimelineRecordAdded, this);
96     }
97
98     // Static
99
100     static displayNameForCategory(category)
101     {
102         switch (category) {
103         case WebInspector.MemoryCategory.Type.JavaScript:
104             return WebInspector.UIString("JavaScript");
105         case WebInspector.MemoryCategory.Type.Images:
106             return WebInspector.UIString("Images");
107         case WebInspector.MemoryCategory.Type.Layers:
108             return WebInspector.UIString("Layers");
109         case WebInspector.MemoryCategory.Type.Page:
110             return WebInspector.UIString("Page");
111         }
112     }
113
114     // Public
115
116     shown()
117     {
118         super.shown();
119
120         this._timelineRuler.updateLayout(WebInspector.View.LayoutReason.Resize);
121     }
122
123     hidden()
124     {
125         super.hidden();
126     }
127
128     closed()
129     {
130         console.assert(this.representedObject instanceof WebInspector.Timeline);
131         this.representedObject.removeEventListener(null, null, this);
132     }
133
134     reset()
135     {
136         super.reset();
137
138         this._maxSize = 0;
139
140         this._cachedLegendRecord = null;
141         this._cachedLegendMaxSize = undefined;
142         this._cachedLegendCurrentSize = undefined;
143
144         this._usageCircleChart.clear();
145         this._usageCircleChart.needsLayout();
146         this._clearUsageLegend();
147
148         this._maxComparisonCircleChart.clear();
149         this._maxComparisonCircleChart.needsLayout();
150         this._clearMaxComparisonLegend();
151
152         for (let categoryView of this._categoryViews)
153             categoryView.clear();
154     }
155
156     get scrollableElements()
157     {
158         return [this.element];
159     }
160
161     // Protected
162
163     get showsFilterBar() { return false; }
164
165     layout()
166     {
167         // Always update timeline ruler.
168         this._timelineRuler.zeroTime = this.zeroTime;
169         this._timelineRuler.startTime = this.startTime;
170         this._timelineRuler.endTime = this.endTime;
171
172         if (!this._didInitializeCategories)
173             return;
174
175         let graphStartTime = this.startTime;
176         let graphEndTime = this.endTime;
177         let graphCurrentTime = this.currentTime;
178         let visibleEndTime = Math.min(this.endTime, this.currentTime);
179
180         let discontinuities = this._recording.discontinuitiesInTimeRange(graphStartTime, visibleEndTime);
181
182         // Don't include the record before the graph start if the graph start is within a gap.
183         let includeRecordBeforeStart = !discontinuities.length || discontinuities[0].startTime > graphStartTime;
184
185         // FIXME: <https://webkit.org/b/153759> Web Inspector: Memory Timelines should better extend to future data
186         let visibleRecords = this.representedObject.recordsInTimeRange(graphStartTime, visibleEndTime, includeRecordBeforeStart);
187         if (!visibleRecords.length)
188             return;
189
190         // Update total usage chart with the last record's data.
191         let lastRecord = visibleRecords.lastValue;
192         let values = [];
193         for (let {size} of lastRecord.categories)
194             values.push(size);
195         this._usageCircleChart.values = values;
196         this._usageCircleChart.updateLayout();
197         this._updateUsageLegend(lastRecord);
198
199         // Update maximum comparison chart.
200         this._maxComparisonCircleChart.values = [lastRecord.totalSize, this._maxSize - lastRecord.totalSize];
201         this._maxComparisonCircleChart.updateLayout();
202         this._updateMaxComparisonLegend(lastRecord.totalSize);
203
204         // FIXME: <https://webkit.org/b/153758> Web Inspector: Memory Timeline View should be responsive / resizable
205         const categoryViewWidth = 800;
206         const categoryViewHeight = 75;
207
208         let secondsPerPixel = (graphEndTime - graphStartTime) / categoryViewWidth;
209
210         let categoryDataMap = {};
211         for (let categoryView of this._categoryViews)
212             categoryDataMap[categoryView.category] = {dataPoints: [], max: -Infinity, min: Infinity};
213
214         for (let record of visibleRecords) {
215             let time = record.startTime;
216             let discontinuity = null;
217             if (discontinuities.length && discontinuities[0].endTime < time)
218                 discontinuity = discontinuities.shift();
219
220             for (let category of record.categories) {
221                 let categoryData = categoryDataMap[category.type];
222
223                 if (discontinuity) {
224                     if (categoryData.dataPoints.length) {
225                         let previousDataPoint = categoryData.dataPoints.lastValue;
226                         categoryData.dataPoints.push({time: discontinuity.startTime, size: previousDataPoint.size});
227                     }
228
229                     categoryData.dataPoints.push({time: discontinuity.startTime, size: 0});
230                     categoryData.dataPoints.push({time: discontinuity.endTime, size: 0});
231                     categoryData.dataPoints.push({time: discontinuity.endTime, size: category.size});
232                 }
233
234                 categoryData.dataPoints.push({time, size: category.size});
235                 categoryData.max = Math.max(categoryData.max, category.size);
236                 categoryData.min = Math.min(categoryData.min, category.size);
237             }
238         }
239
240         // If the graph end time is inside a gap, the last data point should
241         // only be extended to the start of the discontinuity.
242         if (discontinuities.length)
243             visibleEndTime = discontinuities[0].startTime;
244
245         function layoutCategoryView(categoryView, categoryData) {
246             let {dataPoints, min, max} = categoryData;
247
248             if (min === Infinity)
249                 min = 0;
250             if (max === -Infinity)
251                 max = 0;
252
253             // Zoom in to the top of each graph to accentuate small changes.
254             let graphMin = min * 0.95;
255             let graphMax = (max * 1.05) - graphMin;
256
257             function xScale(time) {
258                 return (time - graphStartTime) / secondsPerPixel;
259             }
260             function yScale(size) {
261                 return categoryViewHeight - (((size - graphMin) / graphMax) * categoryViewHeight);
262             }
263
264             categoryView.layoutWithDataPoints(dataPoints, visibleEndTime, min, max, xScale, yScale);
265         }
266
267         for (let categoryView of this._categoryViews)
268             layoutCategoryView(categoryView, categoryDataMap[categoryView.category]);
269     }
270
271     // Private
272
273     _clearUsageLegend()
274     {
275         for (let sizeElement of this._usageLegendSizeElementMap.values())
276             sizeElement.textContent = emDash;
277
278         let totalElement = this._usageCircleChart.centerElement.firstChild;
279         if (totalElement) {
280             totalElement.firstChild.textContent = "";
281             totalElement.lastChild.textContent = "";
282         }
283     }
284
285     _updateUsageLegend(record)
286     {
287         if (this._cachedLegendRecord === record)
288             return;
289
290         this._cachedLegendRecord = record;
291
292         for (let {type, size} of record.categories) {
293             let sizeElement = this._usageLegendSizeElementMap.get(type);
294             sizeElement.textContent = Number.isFinite(size) ? Number.bytesToString(size) : emDash;
295         }
296
297         let centerElement = this._usageCircleChart.centerElement;
298         let totalElement = centerElement.firstChild;
299         if (!totalElement) {
300             totalElement = centerElement.appendChild(document.createElement("div"));
301             totalElement.classList.add("total-usage");
302             totalElement.appendChild(document.createElement("span")); // firstChild
303             totalElement.appendChild(document.createElement("br"));
304             totalElement.appendChild(document.createElement("span")); // lastChild
305         }
306
307         let totalSize = Number.bytesToString(record.totalSize).split(/\s+/);
308         totalElement.firstChild.textContent = totalSize[0];
309         totalElement.lastChild.textContent = totalSize[1];
310     }
311
312     _clearMaxComparisonLegend()
313     {
314         this._maxComparisonMaximumSizeElement.textContent = emDash;
315         this._maxComparisonCurrentSizeElement.textContent = emDash;
316
317         let totalElement = this._maxComparisonCircleChart.centerElement.firstChild;
318         if (totalElement)
319             totalElement.textContent = "";
320     }
321
322     _updateMaxComparisonLegend(currentSize)
323     {
324         if (this._cachedLegendMaxSize === this._maxSize && this._cachedLegendCurrentSize === currentSize)
325             return;
326
327         this._cachedLegendMaxSize = this._maxSize;
328         this._cachedLegendCurrentSize = currentSize;
329
330         this._maxComparisonMaximumSizeElement.textContent = Number.isFinite(this._maxSize) ? Number.bytesToString(this._maxSize) : emDash;
331         this._maxComparisonCurrentSizeElement.textContent = Number.isFinite(currentSize) ? Number.bytesToString(currentSize) : emDash;
332
333         let centerElement = this._maxComparisonCircleChart.centerElement;
334         let totalElement = centerElement.firstChild;
335         if (!totalElement) {
336             totalElement = centerElement.appendChild(document.createElement("div"));
337             totalElement.classList.add("max-percentage");
338         }
339
340         // The chart will only show a perfect circle if the current and max are really the same value.
341         // So do a little massaging to ensure 99.95 doesn't get rounded up to 100.
342         let percent = ((currentSize / this._maxSize) * 100);
343         totalElement.textContent = (percent === 100 ? percent : (percent - 0.05).toFixed(1)) + "%";
344     }
345
346     _initializeCategoryViews(record)
347     {
348         console.assert(!this._didInitializeCategories, "Should only initialize category views once");
349         this._didInitializeCategories = true;
350
351         let segments = [];
352         let lastCategoryViewElement = null;
353
354         function appendLegendRow(legendElement, swatchClass, label, tooltip) {
355             let rowElement = legendElement.appendChild(document.createElement("div"));
356             rowElement.classList.add("row");
357             let swatchElement = rowElement.appendChild(document.createElement("div"));
358             swatchElement.classList.add("swatch", swatchClass);
359             let labelElement = rowElement.appendChild(document.createElement("p"));
360             labelElement.classList.add("label");
361             labelElement.textContent = label;
362             let sizeElement = rowElement.appendChild(document.createElement("p"));
363             sizeElement.classList.add("size");
364
365             if (tooltip)
366                 rowElement.title = tooltip;
367
368             return sizeElement;
369         }
370
371         for (let {type} of record.categories) {
372             segments.push(type);
373
374             // Per-category graph.
375             let categoryView = new WebInspector.MemoryCategoryView(type, WebInspector.MemoryTimelineView.displayNameForCategory(type));
376             this._categoryViews.push(categoryView);
377             if (!lastCategoryViewElement)
378                 this._detailsContainerElement.appendChild(categoryView.element);
379             else
380                 this._detailsContainerElement.insertBefore(categoryView.element, lastCategoryViewElement);
381             lastCategoryViewElement = categoryView.element;
382
383             // Usage legend rows.
384             let sizeElement = appendLegendRow.call(this, this._usageLegendElement, type, WebInspector.MemoryTimelineView.displayNameForCategory(type));
385             this._usageLegendSizeElementMap.set(type, sizeElement);
386         }
387
388         this._usageCircleChart.segments = segments;
389
390         // Max comparison legend rows.
391         this._maxComparisonCircleChart.segments = ["current", "remainder"];
392         this._maxComparisonMaximumSizeElement = appendLegendRow.call(this, this._maxComparisonLegendElement, "remainder", WebInspector.UIString("Maximum"), WebInspector.UIString("Maximum maximum memory size in this recording"));
393         this._maxComparisonCurrentSizeElement = appendLegendRow.call(this, this._maxComparisonLegendElement, "current", WebInspector.UIString("Current"), WebInspector.UIString("Total memory size at the end of the selected time range"));
394     }
395
396     _memoryTimelineRecordAdded(event)
397     {
398         let memoryTimelineRecord = event.data.record;
399         console.assert(memoryTimelineRecord instanceof WebInspector.MemoryTimelineRecord);
400
401         if (!this._didInitializeCategories)
402             this._initializeCategoryViews(memoryTimelineRecord);
403
404         this._maxSize = Math.max(this._maxSize, memoryTimelineRecord.totalSize);
405
406         if (memoryTimelineRecord.startTime >= this.startTime && memoryTimelineRecord.endTime <= this.endTime)
407             this.needsLayout();
408     }
409 };