| /* |
| * Copyright (C) 2016 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.MemoryTimelineView = class MemoryTimelineView extends WI.TimelineView |
| { |
| constructor(timeline, extraArguments) |
| { |
| super(timeline, extraArguments); |
| |
| this._recording = extraArguments.recording; |
| |
| console.assert(timeline.type === WI.TimelineRecord.Type.Memory, timeline); |
| |
| this.element.classList.add("memory"); |
| |
| 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; |
| } |
| |
| let usageTooltip = WI.UIString("Breakdown of each memory category at the end of the selected time range"); |
| let usageChartContainerElement = createChartContainer(overviewElement, WI.UIString("Breakdown"), usageTooltip); |
| this._usageCircleChart = new WI.CircleChart({size: 120, innerRadiusRatio: 0.5}); |
| this.addSubview(this._usageCircleChart); |
| usageChartContainerElement.appendChild(this._usageCircleChart.element); |
| this._usageLegendElement = usageChartContainerElement.appendChild(document.createElement("div")); |
| this._usageLegendElement.classList.add("legend", "usage"); |
| |
| let dividerElement = overviewElement.appendChild(document.createElement("div")); |
| dividerElement.classList.add("divider"); |
| |
| let maxComparisonTooltip = WI.UIString("Comparison of total memory size at the end of the selected time range to the maximum memory size in this recording"); |
| let maxComparisonChartContainerElement = createChartContainer(overviewElement, WI.UIString("Max Comparison"), maxComparisonTooltip); |
| this._maxComparisonCircleChart = new WI.CircleChart({size: 120, innerRadiusRatio: 0.5}); |
| this.addSubview(this._maxComparisonCircleChart); |
| maxComparisonChartContainerElement.appendChild(this._maxComparisonCircleChart.element); |
| this._maxComparisonLegendElement = maxComparisonChartContainerElement.appendChild(document.createElement("div")); |
| this._maxComparisonLegendElement.classList.add("legend", "maximum"); |
| |
| 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); |
| |
| let detailsSubtitleElement = detailsContainerElement.appendChild(document.createElement("div")); |
| detailsSubtitleElement.classList.add("subtitle"); |
| detailsSubtitleElement.textContent = WI.UIString("Categories"); |
| |
| this._didInitializeCategories = false; |
| this._categoryViews = []; |
| this._usageLegendSizeElementMap = new Map; |
| |
| this._maxSize = 0; |
| this._maxComparisonMaximumSizeElement = null; |
| this._maxComparisonCurrentSizeElement = null; |
| |
| timeline.addEventListener(WI.Timeline.Event.RecordAdded, this._memoryTimelineRecordAdded, this); |
| |
| this.element.addEventListener("mousemove", this._handleGraphMouseMove.bind(this)); |
| |
| for (let record of timeline.records) |
| this._processRecord(record); |
| } |
| |
| // Static |
| |
| static displayNameForCategory(category) |
| { |
| switch (category) { |
| case WI.MemoryCategory.Type.JavaScript: |
| return WI.UIString("JavaScript"); |
| case WI.MemoryCategory.Type.Images: |
| return WI.UIString("Images"); |
| case WI.MemoryCategory.Type.Layers: |
| return WI.UIString("Layers"); |
| case WI.MemoryCategory.Type.Page: |
| return WI.UIString("Page"); |
| } |
| } |
| |
| static get memoryCategoryViewHeight() { return 75; } |
| |
| // Public |
| |
| shown() |
| { |
| super.shown(); |
| |
| this._timelineRuler.updateLayout(WI.View.LayoutReason.Resize); |
| } |
| |
| closed() |
| { |
| console.assert(this.representedObject instanceof WI.Timeline); |
| this.representedObject.removeEventListener(null, null, this); |
| } |
| |
| reset() |
| { |
| super.reset(); |
| |
| this._maxSize = 0; |
| |
| this.clear(); |
| } |
| |
| clear() |
| { |
| this._cachedLegendRecord = null; |
| this._cachedLegendMaxSize = undefined; |
| this._cachedLegendCurrentSize = undefined; |
| |
| this._usageCircleChart.clear(); |
| this._usageCircleChart.needsLayout(); |
| this._clearUsageLegend(); |
| |
| this._maxComparisonCircleChart.clear(); |
| this._maxComparisonCircleChart.needsLayout(); |
| this._clearMaxComparisonLegend(); |
| |
| for (let categoryView of this._categoryViews) |
| categoryView.clear(); |
| } |
| |
| get scrollableElements() |
| { |
| return [this.element]; |
| } |
| |
| // Protected |
| |
| get showsFilterBar() { return false; } |
| |
| initialLayout() |
| { |
| super.initialLayout(); |
| |
| this.element.style.setProperty("--memory-category-view-height", MemoryTimelineView.memoryCategoryViewHeight + "px"); |
| } |
| |
| layout() |
| { |
| if (this.layoutReason === WI.View.LayoutReason.Resize) |
| return; |
| |
| // Always update timeline ruler. |
| this._timelineRuler.zeroTime = this.zeroTime; |
| this._timelineRuler.startTime = this.startTime; |
| this._timelineRuler.endTime = this.endTime; |
| |
| if (!this._didInitializeCategories) |
| 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 visibleRecords = this.representedObject.recordsInTimeRange(graphStartTime, visibleEndTime, { |
| includeRecordBeforeStart: !discontinuities.length || discontinuities[0].startTime > graphStartTime, |
| includeRecordAfterEnd: true, |
| }); |
| if (!visibleRecords.length || (visibleRecords.length === 1 && visibleRecords[0].endTime < graphStartTime)) { |
| this.clear(); |
| return; |
| } |
| |
| // Update total usage chart with the last record's data. |
| let lastRecord = visibleRecords.lastValue; |
| let values = []; |
| for (let {size} of lastRecord.categories) |
| values.push(size); |
| this._usageCircleChart.values = values; |
| this._usageCircleChart.updateLayout(); |
| this._updateUsageLegend(lastRecord); |
| |
| // Update maximum comparison chart. |
| this._maxComparisonCircleChart.values = [lastRecord.totalSize, this._maxSize - lastRecord.totalSize]; |
| this._maxComparisonCircleChart.updateLayout(); |
| this._updateMaxComparisonLegend(lastRecord.totalSize); |
| |
| let categoryDataMap = {}; |
| for (let categoryView of this._categoryViews) |
| categoryDataMap[categoryView.category] = {dataPoints: [], max: -Infinity, min: Infinity}; |
| |
| for (let record of visibleRecords) { |
| let time = record.startTime; |
| let startDiscontinuity = null; |
| let endDiscontinuity = null; |
| if (discontinuities.length && discontinuities[0].endTime <= time) { |
| startDiscontinuity = discontinuities.shift(); |
| endDiscontinuity = startDiscontinuity; |
| while (discontinuities.length && discontinuities[0].endTime <= time) |
| endDiscontinuity = discontinuities.shift(); |
| } |
| |
| for (let category of record.categories) { |
| let categoryData = categoryDataMap[category.type]; |
| |
| if (startDiscontinuity) { |
| if (categoryData.dataPoints.length) { |
| let previousDataPoint = categoryData.dataPoints.lastValue; |
| categoryData.dataPoints.push({time: startDiscontinuity.startTime, size: previousDataPoint.size}); |
| } |
| |
| categoryData.dataPoints.push({time: startDiscontinuity.startTime, size: 0}); |
| categoryData.dataPoints.push({time: endDiscontinuity.endTime, size: 0}); |
| categoryData.dataPoints.push({time: endDiscontinuity.endTime, size: category.size}); |
| } |
| |
| categoryData.dataPoints.push({time, size: category.size}); |
| categoryData.max = Math.max(categoryData.max, category.size); |
| categoryData.min = Math.min(categoryData.min, category.size); |
| } |
| } |
| |
| // 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 layoutCategoryView(categoryView, {dataPoints, min, max}) { |
| if (min === Infinity) |
| min = 0; |
| if (max === -Infinity) |
| max = 0; |
| |
| // Zoom in to the top of each graph to accentuate small changes. |
| let graphMin = min * 0.95; |
| let graphMax = (max * 1.05) - graphMin; |
| |
| function xScale(time) { |
| return (time - graphStartTime) / secondsPerPixel; |
| } |
| |
| let size = new WI.Size(xScale(graphEndTime), MemoryTimelineView.memoryCategoryViewHeight); |
| |
| function yScale(value) { |
| return size.height - (((value - graphMin) / graphMax) * size.height); |
| } |
| |
| categoryView.updateChart(dataPoints, size, visibleEndTime, min, max, xScale, yScale); |
| } |
| |
| for (let categoryView of this._categoryViews) |
| layoutCategoryView(categoryView, categoryDataMap[categoryView.category]); |
| } |
| |
| // Private |
| |
| _graphPositionForMouseEvent(event) |
| { |
| let chartElement = event.target.closest(".area-chart, .stacked-area-chart, .range-chart"); |
| if (!chartElement) |
| return NaN; |
| |
| let chartRect = chartElement.getBoundingClientRect(); |
| let position = event.pageX - chartRect.left; |
| |
| if (WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL) |
| return chartRect.width - position; |
| return position; |
| } |
| |
| _handleGraphMouseMove(event) |
| { |
| let mousePosition = this._graphPositionForMouseEvent(event); |
| if (isNaN(mousePosition)) { |
| this.dispatchEventToListeners(WI.TimelineView.Event.ScannerHide); |
| return; |
| } |
| |
| let secondsPerPixel = this._timelineRuler.secondsPerPixel; |
| let time = this.startTime + (mousePosition * secondsPerPixel); |
| |
| this.dispatchEventToListeners(WI.TimelineView.Event.ScannerShow, {time}); |
| } |
| |
| _clearUsageLegend() |
| { |
| for (let sizeElement of this._usageLegendSizeElementMap.values()) |
| sizeElement.textContent = emDash; |
| |
| let totalElement = this._usageCircleChart.centerElement.firstChild; |
| if (totalElement) { |
| totalElement.firstChild.textContent = ""; |
| totalElement.lastChild.textContent = ""; |
| } |
| } |
| |
| _updateUsageLegend(record) |
| { |
| if (this._cachedLegendRecord === record) |
| return; |
| |
| this._cachedLegendRecord = record; |
| |
| for (let {type, size} of record.categories) { |
| let sizeElement = this._usageLegendSizeElementMap.get(type); |
| sizeElement.textContent = Number.isFinite(size) ? Number.bytesToString(size) : emDash; |
| } |
| |
| let centerElement = this._usageCircleChart.centerElement; |
| let totalElement = centerElement.firstChild; |
| if (!totalElement) { |
| totalElement = centerElement.appendChild(document.createElement("div")); |
| totalElement.classList.add("total-usage"); |
| totalElement.appendChild(document.createElement("span")); // firstChild |
| totalElement.appendChild(document.createElement("br")); |
| totalElement.appendChild(document.createElement("span")); // lastChild |
| } |
| |
| let totalSize = Number.bytesToString(record.totalSize).split(/\s+/); |
| totalElement.firstChild.textContent = totalSize[0]; |
| totalElement.lastChild.textContent = totalSize[1]; |
| } |
| |
| _clearMaxComparisonLegend() |
| { |
| if (this._maxComparisonMaximumSizeElement) |
| this._maxComparisonMaximumSizeElement.textContent = emDash; |
| if (this._maxComparisonCurrentSizeElement) |
| this._maxComparisonCurrentSizeElement.textContent = emDash; |
| |
| let totalElement = this._maxComparisonCircleChart.centerElement.firstChild; |
| if (totalElement) |
| totalElement.textContent = ""; |
| } |
| |
| _updateMaxComparisonLegend(currentSize) |
| { |
| if (this._cachedLegendMaxSize === this._maxSize && this._cachedLegendCurrentSize === currentSize) |
| return; |
| |
| this._cachedLegendMaxSize = this._maxSize; |
| this._cachedLegendCurrentSize = currentSize; |
| |
| this._maxComparisonMaximumSizeElement.textContent = Number.isFinite(this._maxSize) ? Number.bytesToString(this._maxSize) : emDash; |
| this._maxComparisonCurrentSizeElement.textContent = Number.isFinite(currentSize) ? Number.bytesToString(currentSize) : emDash; |
| |
| let centerElement = this._maxComparisonCircleChart.centerElement; |
| let totalElement = centerElement.firstChild; |
| if (!totalElement) { |
| totalElement = centerElement.appendChild(document.createElement("div")); |
| totalElement.classList.add("max-percentage"); |
| } |
| |
| // The chart will only show a perfect circle if the current and max are really the same value. |
| // So do a little massaging to ensure 0.9995 doesn't get rounded up to 1. |
| let percent = currentSize / this._maxSize; |
| totalElement.textContent = Number.percentageString(percent === 1 ? percent : (percent - 0.0005)); |
| } |
| |
| _initializeCategoryViews(record) |
| { |
| console.assert(!this._didInitializeCategories, "Should only initialize category views once"); |
| this._didInitializeCategories = true; |
| |
| let segments = []; |
| let lastCategoryViewElement = null; |
| |
| function appendLegendRow(legendElement, swatchClass, label, tooltip) { |
| let rowElement = legendElement.appendChild(document.createElement("div")); |
| rowElement.classList.add("row"); |
| |
| let swatchElement = rowElement.appendChild(document.createElement("div")); |
| swatchElement.classList.add("swatch", swatchClass); |
| |
| let valueContainer = rowElement.appendChild(document.createElement("div")); |
| valueContainer.classList.add("value"); |
| |
| let labelElement = valueContainer.appendChild(document.createElement("div")); |
| labelElement.classList.add("label"); |
| labelElement.textContent = label; |
| |
| let sizeElement = valueContainer.appendChild(document.createElement("div")); |
| sizeElement.classList.add("size"); |
| |
| if (tooltip) |
| rowElement.title = tooltip; |
| |
| return sizeElement; |
| } |
| |
| for (let {type} of record.categories) { |
| segments.push(type); |
| |
| // Per-category graph. |
| let categoryView = new WI.MemoryCategoryView(type, WI.MemoryTimelineView.displayNameForCategory(type)); |
| this._categoryViews.push(categoryView); |
| this.addSubview(categoryView); |
| if (!lastCategoryViewElement) |
| this._detailsContainerElement.appendChild(categoryView.element); |
| else |
| this._detailsContainerElement.insertBefore(categoryView.element, lastCategoryViewElement); |
| lastCategoryViewElement = categoryView.element; |
| |
| // Usage legend rows. |
| let sizeElement = appendLegendRow.call(this, this._usageLegendElement, type, WI.MemoryTimelineView.displayNameForCategory(type)); |
| this._usageLegendSizeElementMap.set(type, sizeElement); |
| } |
| |
| this._usageCircleChart.segments = segments; |
| |
| // Max comparison legend rows. |
| this._maxComparisonCircleChart.segments = ["current", "remainder"]; |
| this._maxComparisonMaximumSizeElement = appendLegendRow.call(this, this._maxComparisonLegendElement, "remainder", WI.UIString("Maximum"), WI.UIString("Maximum maximum memory size in this recording")); |
| this._maxComparisonCurrentSizeElement = appendLegendRow.call(this, this._maxComparisonLegendElement, "current", WI.UIString("Current"), WI.UIString("Total memory size at the end of the selected time range")); |
| } |
| |
| _memoryTimelineRecordAdded(event) |
| { |
| let memoryTimelineRecord = event.data.record; |
| console.assert(memoryTimelineRecord instanceof WI.MemoryTimelineRecord); |
| |
| this._processRecord(memoryTimelineRecord); |
| |
| if (memoryTimelineRecord.startTime >= this.startTime && memoryTimelineRecord.endTime <= this.endTime) |
| this.needsLayout(); |
| } |
| |
| _processRecord(memoryTimelineRecord) |
| { |
| if (!this._didInitializeCategories) |
| this._initializeCategoryViews(memoryTimelineRecord); |
| |
| this._maxSize = Math.max(this._maxSize, memoryTimelineRecord.totalSize); |
| } |
| }; |