blob: 4de7ef4d9f4ca308657216c02591abe9b516d907 [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.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
attached()
{
super.attached();
this._timelineRuler.needsLayout(WI.View.LayoutReason.Resize);
}
closed()
{
this.representedObject.removeEventListener(WI.Timeline.Event.RecordAdded, this._memoryTimelineRecordAdded, 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);
}
};