| /* |
| * 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.MemoryTimelineOverviewGraph = class MemoryTimelineOverviewGraph extends WI.TimelineOverviewGraph |
| { |
| constructor(timeline, timelineOverview) |
| { |
| console.assert(timeline instanceof WI.MemoryTimeline); |
| |
| super(timelineOverview); |
| |
| this.element.classList.add("memory"); |
| |
| this._memoryTimeline = timeline; |
| this._memoryTimeline.addEventListener(WI.Timeline.Event.RecordAdded, this._memoryTimelineRecordAdded, this); |
| this._memoryTimeline.addEventListener(WI.MemoryTimeline.Event.MemoryPressureEventAdded, this._memoryTimelineMemoryPressureEventAdded, this); |
| |
| this._didInitializeCategories = false; |
| |
| this._chart = new WI.StackedAreaChart; |
| this._chart.size = new WI.Size(0, this.height); |
| this.addSubview(this._chart); |
| this.element.appendChild(this._chart.element); |
| |
| this._legendElement = this.element.appendChild(document.createElement("div")); |
| this._legendElement.classList.add("legend"); |
| |
| this._memoryPressureMarkersContainerElement = this.element.appendChild(document.createElement("div")); |
| this._memoryPressureMarkersContainerElement.classList.add("memory-pressure-markers-container"); |
| this._memoryPressureMarkerElements = []; |
| |
| this.reset(); |
| |
| for (let record of this._memoryTimeline.records) |
| this._processRecord(record); |
| } |
| |
| // Protected |
| |
| get height() |
| { |
| return 108; |
| } |
| |
| reset() |
| { |
| super.reset(); |
| |
| this._maxSize = 0; |
| this._cachedMaxSize = undefined; |
| |
| this._updateLegend(); |
| this._chart.clear(); |
| this._chart.needsLayout(); |
| |
| this._memoryPressureMarkersContainerElement.removeChildren(); |
| this._memoryPressureMarkerElements = []; |
| } |
| |
| layout() |
| { |
| if (!this.visible) |
| return; |
| |
| this._updateLegend(); |
| this._chart.clear(); |
| |
| if (!this._didInitializeCategories) |
| return; |
| |
| let graphWidth = this.timelineOverview.scrollContainerWidth; |
| if (isNaN(graphWidth)) |
| return; |
| |
| if (this._chart.size.width !== graphWidth || this._chart.size.height !== this.height) |
| this._chart.size = new WI.Size(graphWidth, this.height); |
| |
| let graphStartTime = this.startTime; |
| let visibleEndTime = Math.min(this.endTime, this.currentTime); |
| |
| let secondsPerPixel = this.timelineOverview.secondsPerPixel; |
| let maxCapacity = this._maxSize * 1.05; // Add 5% for padding. |
| |
| function xScale(time) { |
| return (time - graphStartTime) / secondsPerPixel; |
| } |
| |
| let height = this.height; |
| function yScale(size) { |
| return height - ((size / maxCapacity) * height); |
| } |
| |
| let visibleMemoryPressureEventMarkers = this._visibleMemoryPressureEvents(graphStartTime, visibleEndTime); |
| |
| // Reuse existing marker elements. |
| for (let i = 0; i < visibleMemoryPressureEventMarkers.length; ++i) { |
| let markerElement = this._memoryPressureMarkerElements[i]; |
| if (!markerElement) { |
| markerElement = this._memoryPressureMarkersContainerElement.appendChild(document.createElement("div")); |
| markerElement.classList.add("memory-pressure-event"); |
| this._memoryPressureMarkerElements[i] = markerElement; |
| } |
| |
| let memoryPressureEvent = visibleMemoryPressureEventMarkers[i]; |
| let property = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL ? "right" : "left"; |
| markerElement.style.setProperty(property, `${xScale(memoryPressureEvent.timestamp)}px`); |
| } |
| |
| // Remove excess marker elements. |
| let excess = this._memoryPressureMarkerElements.length - visibleMemoryPressureEventMarkers.length; |
| if (excess) { |
| let elementsToRemove = this._memoryPressureMarkerElements.splice(visibleMemoryPressureEventMarkers.length); |
| for (let element of elementsToRemove) |
| element.remove(); |
| } |
| |
| let discontinuities = this.timelineOverview.discontinuitiesInTimeRange(graphStartTime, visibleEndTime); |
| |
| let visibleRecords = this._memoryTimeline.recordsInTimeRange(graphStartTime, visibleEndTime, { |
| includeRecordBeforeStart: !discontinuities.length || discontinuities[0].startTime > graphStartTime, |
| includeRecordAfterEnd: true, |
| }); |
| if (!visibleRecords.length) |
| return; |
| |
| function pointSetForRecord(record) { |
| let size = 0; |
| let ys = []; |
| for (let i = 0; i < record.categories.length; ++i) { |
| size += record.categories[i].size; |
| ys[i] = yScale(size); |
| } |
| return ys; |
| } |
| |
| // Extend the first record to the start so it doesn't look like we originate at zero size. |
| if (visibleRecords[0] === this._memoryTimeline.records[0] && (!discontinuities.length || discontinuities[0].startTime > visibleRecords[0].startTime)) |
| this._chart.addPointSet(0, pointSetForRecord(visibleRecords[0])); |
| |
| function insertDiscontinuity(previousRecord, startDiscontinuity, endDiscontinuity, nextRecord) |
| { |
| console.assert(previousRecord || nextRecord); |
| if (!(previousRecord || nextRecord)) |
| return; |
| |
| let xStart = xScale(startDiscontinuity.startTime); |
| let xEnd = xScale(endDiscontinuity.endTime); |
| |
| // Extend the previous record to the start of the discontinuity. |
| if (previousRecord) |
| this._chart.addPointSet(xStart, pointSetForRecord(previousRecord)); |
| |
| let zeroValues = Array((previousRecord || nextRecord).categories.length).fill(yScale(0)); |
| this._chart.addPointSet(xStart, zeroValues); |
| |
| if (nextRecord) { |
| this._chart.addPointSet(xEnd, zeroValues); |
| this._chart.addPointSet(xEnd, pointSetForRecord(nextRecord)); |
| } else { |
| // Extend the discontinuity to the visible end time to prevent |
| // drawing artifacts when the next record arrives. |
| this._chart.addPointSet(xScale(visibleEndTime), zeroValues); |
| } |
| } |
| |
| // Points for visible records. |
| let previousRecord = null; |
| for (let record of visibleRecords) { |
| if (discontinuities.length && discontinuities[0].endTime <= record.startTime) { |
| let startDiscontinuity = discontinuities.shift(); |
| let endDiscontinuity = startDiscontinuity; |
| while (discontinuities.length && discontinuities[0].endTime <= record.startTime) |
| endDiscontinuity = discontinuities.shift(); |
| insertDiscontinuity.call(this, previousRecord, startDiscontinuity, endDiscontinuity, record); |
| } |
| |
| let x = xScale(record.startTime); |
| this._chart.addPointSet(x, pointSetForRecord(record)); |
| |
| previousRecord = record; |
| } |
| |
| if (discontinuities.length) |
| insertDiscontinuity.call(this, previousRecord, discontinuities[0], discontinuities[0], null); |
| else { |
| // Extend the last value to current / end time. |
| let lastRecord = visibleRecords.lastValue; |
| if (lastRecord.startTime <= visibleEndTime) { |
| let x = Math.floor(xScale(visibleEndTime)); |
| this._chart.addPointSet(x, pointSetForRecord(lastRecord)); |
| } |
| } |
| } |
| |
| // Private |
| |
| _updateLegend() |
| { |
| if (this._cachedMaxSize === this._maxSize) |
| return; |
| |
| this._cachedMaxSize = this._maxSize; |
| |
| if (!this._maxSize) { |
| this._legendElement.hidden = true; |
| this._legendElement.textContent = ""; |
| } else { |
| this._legendElement.hidden = false; |
| this._legendElement.textContent = WI.UIString("Maximum Size: %s").format(Number.bytesToString(this._maxSize)); |
| } |
| } |
| |
| _visibleMemoryPressureEvents(startTime, endTime) |
| { |
| let events = this._memoryTimeline.memoryPressureEvents; |
| if (!events.length) |
| return []; |
| |
| let lowerIndex = events.lowerBound(startTime, (time, event) => time - event.timestamp); |
| let upperIndex = events.upperBound(endTime, (time, event) => time - event.timestamp); |
| return events.slice(lowerIndex, upperIndex); |
| } |
| |
| _memoryTimelineRecordAdded(event) |
| { |
| let memoryTimelineRecord = event.data.record; |
| console.assert(memoryTimelineRecord instanceof WI.MemoryTimelineRecord); |
| |
| this._processRecord(memoryTimelineRecord); |
| |
| this.needsLayout(); |
| } |
| |
| _processRecord(memoryTimelineRecord) |
| { |
| this._maxSize = Math.max(this._maxSize, memoryTimelineRecord.totalSize); |
| |
| if (!this._didInitializeCategories) { |
| this._didInitializeCategories = true; |
| let types = []; |
| for (let category of memoryTimelineRecord.categories) |
| types.push(category.type); |
| this._chart.initializeSections(types); |
| } |
| } |
| |
| _memoryTimelineMemoryPressureEventAdded(event) |
| { |
| this.needsLayout(); |
| } |
| }; |