blob: 496092fff701f06d60e3937ca9067663502c117b [file] [log] [blame]
/*
* 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);
// Don't include the record before the graph start if the graph start is within a gap.
let includeRecordBeforeStart = !discontinuities.length || discontinuities[0].startTime > graphStartTime;
// FIXME: <https://webkit.org/b/153759> Web Inspector: Memory Timelines should better extend to future data
let visibleRecords = this._memoryTimeline.recordsInTimeRange(graphStartTime, visibleEndTime, includeRecordBeforeStart);
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();
}
};