1828 lines
78 KiB
JavaScript
1828 lines
78 KiB
JavaScript
/*
|
|
* Copyright (C) 2019 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.CPUTimelineView = class CPUTimelineView extends WI.TimelineView
|
|
{
|
|
constructor(timeline, extraArguments)
|
|
{
|
|
console.assert(timeline.type === WI.TimelineRecord.Type.CPU, timeline);
|
|
|
|
super(timeline, extraArguments);
|
|
|
|
this._recording = extraArguments.recording;
|
|
|
|
this.element.classList.add("cpu");
|
|
|
|
this._sectionLimit = CPUTimelineView.defaultSectionLimit;
|
|
|
|
this._statisticsData = null;
|
|
this._secondsPerPixelInLayout = undefined;
|
|
this._visibleRecordsInLayout = [];
|
|
this._discontinuitiesInLayout = [];
|
|
|
|
this._stickingOverlay = false;
|
|
this._overlayRecord = null;
|
|
this._overlayTime = NaN;
|
|
|
|
timeline.addEventListener(WI.Timeline.Event.RecordAdded, this._cpuTimelineRecordAdded, this);
|
|
}
|
|
|
|
// Static
|
|
|
|
static displayNameForSampleType(type)
|
|
{
|
|
switch (type) {
|
|
case CPUTimelineView.SampleType.JavaScript:
|
|
return WI.UIString("JavaScript");
|
|
case CPUTimelineView.SampleType.Layout:
|
|
return WI.repeatedUIString.timelineRecordLayout();
|
|
case CPUTimelineView.SampleType.Paint:
|
|
return WI.repeatedUIString.timelineRecordPaint();
|
|
case CPUTimelineView.SampleType.Style:
|
|
return WI.UIString("Styles");
|
|
}
|
|
console.error("Unknown sample type", type);
|
|
}
|
|
|
|
static get cpuUsageViewHeight() { return 135; }
|
|
static get threadCPUUsageViewHeight() { return 65; }
|
|
static get indicatorViewHeight() { return 15; }
|
|
|
|
static get lowEnergyThreshold() { return 3; }
|
|
static get mediumEnergyThreshold() { return 30; }
|
|
static get highEnergyThreshold() { return 100; }
|
|
|
|
static get lowEnergyGraphBoundary() { return 10; }
|
|
static get mediumEnergyGraphBoundary() { return 70; }
|
|
static get highEnergyGraphBoundary() { return 100; }
|
|
|
|
static get defaultSectionLimit() { return 5; }
|
|
|
|
// Public
|
|
|
|
attached()
|
|
{
|
|
super.attached();
|
|
|
|
this._timelineRuler?.needsLayout(WI.View.LayoutReason.Resize);
|
|
}
|
|
|
|
closed()
|
|
{
|
|
this.representedObject.removeEventListener(WI.Timeline.Event.RecordAdded, this._cpuTimelineRecordAdded, this);
|
|
}
|
|
|
|
reset()
|
|
{
|
|
super.reset();
|
|
|
|
this._resetSourcesFilters();
|
|
|
|
this.clear();
|
|
}
|
|
|
|
clear()
|
|
{
|
|
if (!this.didInitialLayout)
|
|
return;
|
|
|
|
this._breakdownChart.clear();
|
|
this._breakdownChart.needsLayout();
|
|
this._clearBreakdownLegend();
|
|
|
|
this._energyChart.clear();
|
|
this._energyChart.needsLayout();
|
|
this._clearEnergyImpactText();
|
|
|
|
this._clearStatistics();
|
|
this._clearSources();
|
|
|
|
function clearUsageView(view) {
|
|
view.clear();
|
|
|
|
let markersElement = view.chart.element.querySelector(".markers");
|
|
if (markersElement)
|
|
markersElement.remove();
|
|
}
|
|
|
|
clearUsageView(this._cpuUsageView);
|
|
clearUsageView(this._mainThreadUsageView);
|
|
clearUsageView(this._webkitThreadUsageView);
|
|
clearUsageView(this._unknownThreadUsageView);
|
|
|
|
this._removeWorkerThreadViews();
|
|
|
|
this._sectionLimit = CPUTimelineView.defaultSectionLimit;
|
|
|
|
this._statisticsData = null;
|
|
this._secondsPerPixelInLayout = undefined;
|
|
this._visibleRecordsInLayout = [];
|
|
this._discontinuitiesInLayout = [];
|
|
|
|
this._stickingOverlay = false;
|
|
this._hideGraphOverlay();
|
|
}
|
|
|
|
// Protected
|
|
|
|
get showsFilterBar() { return false; }
|
|
|
|
get scrollableElements()
|
|
{
|
|
return [this.element];
|
|
}
|
|
|
|
initialLayout()
|
|
{
|
|
super.initialLayout();
|
|
|
|
this.element.style.setProperty("--cpu-usage-combined-view-height", CPUTimelineView.cpuUsageViewHeight + "px");
|
|
this.element.style.setProperty("--cpu-usage-view-height", CPUTimelineView.threadCPUUsageViewHeight + "px");
|
|
this.element.style.setProperty("--cpu-usage-indicator-view-height", CPUTimelineView.indicatorViewHeight + "px");
|
|
|
|
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;
|
|
if (tooltip)
|
|
chartSubtitleElement.title = tooltip;
|
|
|
|
let chartFlexContainerElement = chartElement.appendChild(document.createElement("div"));
|
|
chartFlexContainerElement.classList.add("container");
|
|
return chartFlexContainerElement;
|
|
}
|
|
|
|
function appendLegendRow(legendElement, sampleType) {
|
|
let rowElement = legendElement.appendChild(document.createElement("div"));
|
|
rowElement.classList.add("row");
|
|
|
|
let swatchElement = rowElement.appendChild(document.createElement("div"));
|
|
swatchElement.classList.add("swatch", sampleType);
|
|
|
|
let valueContainer = rowElement.appendChild(document.createElement("div"));
|
|
|
|
let labelElement = valueContainer.appendChild(document.createElement("div"));
|
|
labelElement.classList.add("label");
|
|
labelElement.textContent = WI.CPUTimelineView.displayNameForSampleType(sampleType);
|
|
|
|
let sizeElement = valueContainer.appendChild(document.createElement("div"));
|
|
sizeElement.classList.add("size");
|
|
|
|
return sizeElement;
|
|
}
|
|
|
|
let breakdownChartContainerElement = createChartContainer(overviewElement, WI.UIString("Main Thread"), WI.UIString("Breakdown of time spent on the main thread"));
|
|
this._breakdownChart = new WI.CircleChart({size: 120, innerRadiusRatio: 0.5});
|
|
this._breakdownChart.segments = Object.values(WI.CPUTimelineView.SampleType);
|
|
this.addSubview(this._breakdownChart);
|
|
breakdownChartContainerElement.appendChild(this._breakdownChart.element);
|
|
|
|
this._breakdownLegendElement = breakdownChartContainerElement.appendChild(document.createElement("div"));
|
|
this._breakdownLegendElement.classList.add("legend");
|
|
|
|
this._breakdownLegendScriptElement = appendLegendRow(this._breakdownLegendElement, CPUTimelineView.SampleType.JavaScript);
|
|
this._breakdownLegendLayoutElement = appendLegendRow(this._breakdownLegendElement, CPUTimelineView.SampleType.Layout);
|
|
this._breakdownLegendPaintElement = appendLegendRow(this._breakdownLegendElement, CPUTimelineView.SampleType.Paint);
|
|
this._breakdownLegendStyleElement = appendLegendRow(this._breakdownLegendElement, CPUTimelineView.SampleType.Style);
|
|
|
|
let dividerElement = overviewElement.appendChild(document.createElement("div"));
|
|
dividerElement.classList.add("divider");
|
|
|
|
let energyContainerElement = createChartContainer(overviewElement, WI.UIString("Energy Impact"), WI.UIString("Estimated energy impact."));
|
|
energyContainerElement.classList.add("energy");
|
|
|
|
let energyChartElement = energyContainerElement.parentElement;
|
|
let energySubtitleElement = energyChartElement.firstChild;
|
|
let energyInfoElement = energySubtitleElement.appendChild(document.createElement("span"));
|
|
energyInfoElement.classList.add("info", WI.Popover.IgnoreAutoDismissClassName);
|
|
energyInfoElement.textContent = "?";
|
|
|
|
this._energyInfoPopover = null;
|
|
this._energyInfoPopoverContentElement = null;
|
|
energyInfoElement.addEventListener("click", (event) => {
|
|
if (!this._energyInfoPopover)
|
|
this._energyInfoPopover = new WI.Popover;
|
|
|
|
if (!this._energyInfoPopoverContentElement) {
|
|
this._energyInfoPopoverContentElement = document.createElement("div");
|
|
this._energyInfoPopoverContentElement.className = "energy-info-popover-content";
|
|
|
|
const precision = 0;
|
|
let lowPercent = Number.percentageString(CPUTimelineView.lowEnergyThreshold / 100, precision);
|
|
|
|
let p1 = this._energyInfoPopoverContentElement.appendChild(document.createElement("p"));
|
|
p1.textContent = WI.UIString("Periods of high CPU utilization will rapidly drain battery. Strive to keep idle pages under %s average CPU utilization.").format(lowPercent);
|
|
|
|
let p2 = this._energyInfoPopoverContentElement.appendChild(document.createElement("p"));
|
|
p2.textContent = WI.UIString("There is an incurred energy penalty each time the page enters script. This commonly happens with timers, event handlers, and observers.");
|
|
|
|
let p3 = this._energyInfoPopoverContentElement.appendChild(document.createElement("p"));
|
|
p3.textContent = WI.UIString("To improve CPU utilization reduce or batch workloads when the page is not visible or during times when the page is not being interacted with.");
|
|
}
|
|
|
|
let isRTL = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL;
|
|
let preferredEdges = isRTL ? [WI.RectEdge.MAX_Y, WI.RectEdge.MIN_X] : [WI.RectEdge.MAX_Y, WI.RectEdge.MAX_X];
|
|
let calculateTargetFrame = () => WI.Rect.rectFromClientRect(energyInfoElement.getBoundingClientRect()).pad(3);
|
|
|
|
this._energyInfoPopover.presentNewContentWithFrame(this._energyInfoPopoverContentElement, calculateTargetFrame(), preferredEdges);
|
|
this._energyInfoPopover.windowResizeHandler = () => {
|
|
this._energyInfoPopover.present(calculateTargetFrame(), preferredEdges);
|
|
};
|
|
});
|
|
|
|
this._energyChart = new WI.GaugeChart({
|
|
height: 110,
|
|
strokeWidth: 20,
|
|
segments: [
|
|
{className: "low", limit: CPUTimelineView.lowEnergyGraphBoundary},
|
|
{className: "medium", limit: CPUTimelineView.mediumEnergyGraphBoundary},
|
|
{className: "high", limit: CPUTimelineView.highEnergyGraphBoundary},
|
|
]
|
|
});
|
|
this.addSubview(this._energyChart);
|
|
energyContainerElement.appendChild(this._energyChart.element);
|
|
|
|
let energyTextContainerElement = energyContainerElement.appendChild(document.createElement("div"));
|
|
|
|
this._energyImpactLabelElement = energyTextContainerElement.appendChild(document.createElement("div"));
|
|
this._energyImpactLabelElement.className = "energy-impact";
|
|
|
|
this._energyImpactNumberElement = energyTextContainerElement.appendChild(document.createElement("div"));
|
|
this._energyImpactNumberElement.className = "energy-impact-number";
|
|
|
|
this._energyImpactDurationElement = energyTextContainerElement.appendChild(document.createElement("div"));
|
|
this._energyImpactDurationElement.className = "energy-impact-number";
|
|
|
|
let detailsContainerElement = contentElement.appendChild(document.createElement("div"));
|
|
detailsContainerElement.classList.add("details");
|
|
|
|
this._timelineRuler = new WI.TimelineRuler;
|
|
this._timelineRuler.zeroTime = this.zeroTime;
|
|
this._timelineRuler.startTime = this.startTime;
|
|
this._timelineRuler.endTime = this.endTime;
|
|
|
|
this.addSubview(this._timelineRuler);
|
|
detailsContainerElement.appendChild(this._timelineRuler.element);
|
|
|
|
// Cause the TimelineRuler to layout now so we will have some of its
|
|
// important properties initialized for our layout.
|
|
this._timelineRuler.updateLayout(WI.View.LayoutReason.Resize);
|
|
|
|
let detailsSubtitleElement = detailsContainerElement.appendChild(document.createElement("div"));
|
|
detailsSubtitleElement.classList.add("subtitle");
|
|
detailsSubtitleElement.textContent = WI.UIString("CPU Usage");
|
|
|
|
this._cpuUsageView = new WI.CPUUsageCombinedView(WI.UIString("Total"));
|
|
this.addSubview(this._cpuUsageView);
|
|
detailsContainerElement.appendChild(this._cpuUsageView.element);
|
|
|
|
this._cpuUsageView.rangeChart.element.addEventListener("click", this._handleIndicatorClick.bind(this));
|
|
|
|
this._threadsDetailsElement = detailsContainerElement.appendChild(document.createElement("details"));
|
|
this._threadsDetailsElement.open = WI.settings.cpuTimelineThreadDetailsExpanded.value;
|
|
this._threadsDetailsElement.addEventListener("toggle", (event) => {
|
|
WI.settings.cpuTimelineThreadDetailsExpanded.value = this._threadsDetailsElement.open;
|
|
if (this._threadsDetailsElement.open)
|
|
this.updateLayout(WI.CPUTimelineView.LayoutReason.Internal);
|
|
});
|
|
|
|
let threadsSubtitleElement = this._threadsDetailsElement.appendChild(document.createElement("summary"));
|
|
threadsSubtitleElement.classList.add("subtitle", "threads", "expandable");
|
|
threadsSubtitleElement.textContent = WI.UIString("Threads");
|
|
|
|
this._mainThreadUsageView = new WI.CPUUsageView(WI.UIString("Main Thread"));
|
|
this._mainThreadUsageView.element.classList.add("main-thread");
|
|
this.addSubview(this._mainThreadUsageView);
|
|
this._threadsDetailsElement.appendChild(this._mainThreadUsageView.element);
|
|
|
|
this._webkitThreadUsageView = new WI.CPUUsageView(WI.UIString("WebKit Threads"));
|
|
this.addSubview(this._webkitThreadUsageView);
|
|
this._threadsDetailsElement.appendChild(this._webkitThreadUsageView.element);
|
|
|
|
this._unknownThreadUsageView = new WI.CPUUsageView(WI.UIString("Other Threads"));
|
|
this.addSubview(this._unknownThreadUsageView);
|
|
this._threadsDetailsElement.appendChild(this._unknownThreadUsageView.element);
|
|
|
|
this._workerViews = [];
|
|
|
|
this._sourcesFilter = {
|
|
timer: new Set,
|
|
event: new Set,
|
|
observer: new Set,
|
|
};
|
|
|
|
let bottomOverviewElement = contentElement.appendChild(document.createElement("div"));
|
|
bottomOverviewElement.classList.add("overview");
|
|
|
|
let statisticsContainerElement = createChartContainer(bottomOverviewElement, WI.UIString("Statistics"));
|
|
statisticsContainerElement.classList.add("stats");
|
|
|
|
this._statisticsTable = statisticsContainerElement.appendChild(document.createElement("table"));
|
|
this._statisticsRows = [];
|
|
|
|
{
|
|
let {headerCell, numberCell} = this._createTableRow(this._statisticsTable);
|
|
headerCell.textContent = WI.UIString("Network Requests:");
|
|
this._networkRequestsNumberElement = numberCell;
|
|
}
|
|
{
|
|
let {headerCell, numberCell} = this._createTableRow(this._statisticsTable);
|
|
headerCell.textContent = WI.UIString("Script Entries:");
|
|
this._scriptEntriesNumberElement = numberCell;
|
|
}
|
|
|
|
this._clearStatistics();
|
|
|
|
let bottomDividerElement = bottomOverviewElement.appendChild(document.createElement("div"));
|
|
bottomDividerElement.classList.add("divider");
|
|
|
|
let sourcesContainerElement = createChartContainer(bottomOverviewElement, WI.UIString("Sources"));
|
|
sourcesContainerElement.classList.add("stats");
|
|
|
|
this._sourcesTable = sourcesContainerElement.appendChild(document.createElement("table"));
|
|
this._sourcesRows = [];
|
|
|
|
{
|
|
let {row, headerCell, numberCell, labelCell} = this._createTableRow(this._sourcesTable);
|
|
headerCell.textContent = WI.UIString("Filter:");
|
|
this._sourcesFilterRow = row;
|
|
this._sourcesFilterRow.hidden = true;
|
|
this._sourcesFilterNumberElement = numberCell;
|
|
this._sourcesFilterLabelElement = labelCell;
|
|
|
|
let filterClearElement = numberCell.appendChild(document.createElement("span"));
|
|
filterClearElement.className = "filter-clear";
|
|
filterClearElement.textContent = multiplicationSign;
|
|
filterClearElement.addEventListener("click", (event) => {
|
|
this._resetSourcesFilters();
|
|
this._layoutStatisticsAndSources();
|
|
});
|
|
}
|
|
{
|
|
let {row, headerCell, numberCell, labelCell} = this._createTableRow(this._sourcesTable);
|
|
headerCell.textContent = WI.UIString("Timers:");
|
|
this._timerInstallationsRow = row;
|
|
this._timerInstallationsNumberElement = numberCell;
|
|
this._timerInstallationsLabelElement = labelCell;
|
|
}
|
|
{
|
|
let {row, headerCell, numberCell, labelCell} = this._createTableRow(this._sourcesTable);
|
|
headerCell.textContent = WI.UIString("Event Handlers:");
|
|
this._eventHandlersRow = row;
|
|
this._eventHandlersNumberElement = numberCell;
|
|
this._eventHandlersLabelElement = labelCell;
|
|
}
|
|
{
|
|
let {row, headerCell, numberCell, labelCell} = this._createTableRow(this._sourcesTable);
|
|
headerCell.textContent = WI.UIString("Observer Handlers:");
|
|
this._observerHandlersRow = row;
|
|
this._observerHandlersNumberElement = numberCell;
|
|
this._observerHandlersLabelElement = labelCell;
|
|
}
|
|
|
|
this._clearSources();
|
|
|
|
this.element.addEventListener("click", this._handleGraphClick.bind(this));
|
|
this.element.addEventListener("mousemove", this._handleGraphMouseMove.bind(this));
|
|
|
|
this._overlayMarker = new WI.TimelineMarker(-1, WI.TimelineMarker.Type.TimeStamp);
|
|
this._timelineRuler.addMarker(this._overlayMarker);
|
|
}
|
|
|
|
layout()
|
|
{
|
|
if (this.layoutReason === WI.View.LayoutReason.Resize)
|
|
return;
|
|
|
|
if (this.layoutReason !== WI.CPUTimelineView.LayoutReason.Internal)
|
|
this._sectionLimit = CPUTimelineView.defaultSectionLimit;
|
|
|
|
// Always update timeline ruler.
|
|
this._timelineRuler.zeroTime = this.zeroTime;
|
|
this._timelineRuler.startTime = this.startTime;
|
|
this._timelineRuler.endTime = this.endTime;
|
|
|
|
let secondsPerPixel = this._timelineRuler.secondsPerPixel;
|
|
if (!secondsPerPixel)
|
|
return;
|
|
|
|
let graphStartTime = this.startTime;
|
|
let graphEndTime = this.endTime;
|
|
|
|
// When viewing a timeline recording from JSON, this.currentTime is always 0.
|
|
let visibleEndTime = Math.min(this.endTime, this.currentTime) || this.endTime;
|
|
let visibleDuration = visibleEndTime - graphStartTime;
|
|
|
|
let discontinuities = this._recording.discontinuitiesInTimeRange(graphStartTime, visibleEndTime);
|
|
let originalDiscontinuities = discontinuities.slice();
|
|
|
|
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;
|
|
}
|
|
|
|
this._secondsPerPixelInLayout = secondsPerPixel;
|
|
this._visibleRecordsInLayout = visibleRecords;
|
|
this._discontinuitiesInLayout = discontinuities.slice();
|
|
|
|
this._statisticsData = this._computeStatisticsData(graphStartTime, visibleEndTime);
|
|
this._layoutBreakdownChart();
|
|
this._layoutStatisticsAndSources();
|
|
|
|
let dataPoints = [];
|
|
let workersDataMap = new Map;
|
|
let workersSeenInCurrentRecord = new Set;
|
|
|
|
let max = -Infinity;
|
|
let mainThreadMax = -Infinity;
|
|
let webkitThreadMax = -Infinity;
|
|
let unknownThreadMax = -Infinity;
|
|
let workerMax = -Infinity;
|
|
|
|
let min = Infinity;
|
|
let mainThreadMin = Infinity;
|
|
let webkitThreadMin = Infinity;
|
|
let unknownThreadMin = Infinity;
|
|
|
|
let average = 0;
|
|
let mainThreadAverage = 0;
|
|
let webkitThreadAverage = 0;
|
|
let unknownThreadAverage = 0;
|
|
|
|
for (let record of visibleRecords) {
|
|
let time = record.timestamp;
|
|
let {usage, mainThreadUsage, workerThreadUsage, webkitThreadUsage, unknownThreadUsage} = record;
|
|
|
|
if (discontinuities.length && discontinuities[0].endTime <= time) {
|
|
let startDiscontinuity = discontinuities.shift();
|
|
let endDiscontinuity = startDiscontinuity;
|
|
while (discontinuities.length && discontinuities[0].endTime <= time)
|
|
endDiscontinuity = discontinuities.shift();
|
|
|
|
if (dataPoints.length) {
|
|
let previousDataPoint = dataPoints.lastValue;
|
|
dataPoints.push({
|
|
time: startDiscontinuity.startTime,
|
|
mainThreadUsage: previousDataPoint.mainThreadUsage,
|
|
workerThreadUsage: previousDataPoint.workerThreadUsage,
|
|
webkitThreadUsage: previousDataPoint.webkitThreadUsage,
|
|
unknownThreadUsage: previousDataPoint.unknownThreadUsage,
|
|
usage: previousDataPoint.usage,
|
|
});
|
|
}
|
|
|
|
dataPoints.push({time: startDiscontinuity.startTime, mainThreadUsage: 0, workerThreadUsage: 0, webkitThreadUsage: 0, unknownThreadUsage: 0, usage: 0});
|
|
dataPoints.push({time: endDiscontinuity.endTime, mainThreadUsage: 0, workerThreadUsage: 0, webkitThreadUsage: 0, unknownThreadUsage: 0, usage: 0});
|
|
dataPoints.push({time: endDiscontinuity.endTime, mainThreadUsage, workerThreadUsage, webkitThreadUsage, unknownThreadUsage, usage});
|
|
}
|
|
|
|
dataPoints.push({time, mainThreadUsage, workerThreadUsage, webkitThreadUsage, unknownThreadUsage, usage});
|
|
|
|
max = Math.max(max, usage);
|
|
mainThreadMax = Math.max(mainThreadMax, mainThreadUsage);
|
|
webkitThreadMax = Math.max(webkitThreadMax, webkitThreadUsage);
|
|
unknownThreadMax = Math.max(unknownThreadMax, unknownThreadUsage);
|
|
|
|
min = Math.min(min, usage);
|
|
mainThreadMin = Math.min(mainThreadMin, mainThreadUsage);
|
|
webkitThreadMin = Math.min(webkitThreadMin, webkitThreadUsage);
|
|
unknownThreadMin = Math.min(unknownThreadMin, unknownThreadUsage);
|
|
|
|
average += usage;
|
|
mainThreadAverage += mainThreadUsage;
|
|
webkitThreadAverage += webkitThreadUsage;
|
|
unknownThreadAverage += unknownThreadUsage;
|
|
|
|
let workersSeenInLastRecord = workersSeenInCurrentRecord;
|
|
workersSeenInCurrentRecord = new Set;
|
|
|
|
if (record.workersData && record.workersData.length) {
|
|
for (let {targetId, usage} of record.workersData) {
|
|
workersSeenInCurrentRecord.add(targetId);
|
|
let workerData = workersDataMap.get(targetId);
|
|
if (!workerData) {
|
|
workerData = {
|
|
discontinuities: originalDiscontinuities.slice(),
|
|
recordsCount: 0,
|
|
dataPoints: [],
|
|
min: Infinity,
|
|
max: -Infinity,
|
|
average: 0
|
|
};
|
|
|
|
while (workerData.discontinuities.length && workerData.discontinuities[0].endTime <= graphStartTime)
|
|
workerData.discontinuities.shift();
|
|
workerData.dataPoints.push({time: graphStartTime, usage: 0});
|
|
workerData.dataPoints.push({time, usage: 0});
|
|
workersDataMap.set(targetId, workerData);
|
|
}
|
|
|
|
if (workerData.discontinuities.length && workerData.discontinuities[0].endTime < time) {
|
|
let startDiscontinuity = workerData.discontinuities.shift();
|
|
let endDiscontinuity = startDiscontinuity;
|
|
while (workerData.discontinuities.length && workerData.discontinuities[0].endTime < time)
|
|
endDiscontinuity = workerData.discontinuities.shift();
|
|
if (workerData.dataPoints.length) {
|
|
let previousDataPoint = workerData.dataPoints.lastValue;
|
|
workerData.dataPoints.push({time: startDiscontinuity.startTime, usage: previousDataPoint.usage});
|
|
}
|
|
workerData.dataPoints.push({time: startDiscontinuity.startTime, usage: 0});
|
|
workerData.dataPoints.push({time: endDiscontinuity.endTime, usage: 0});
|
|
workerData.dataPoints.push({time: endDiscontinuity.endTime, usage});
|
|
}
|
|
|
|
workerData.dataPoints.push({time, usage});
|
|
workerData.recordsCount += 1;
|
|
workerData.max = Math.max(workerData.max, usage);
|
|
workerData.min = Math.min(workerData.min, usage);
|
|
workerData.average += usage;
|
|
}
|
|
}
|
|
|
|
// Close any worker that died by dropping to zero.
|
|
if (workersSeenInLastRecord.size) {
|
|
let deadWorkers = workersSeenInLastRecord.difference(workersSeenInCurrentRecord);
|
|
for (let workerId of deadWorkers) {
|
|
let workerData = workersDataMap.get(workerId);
|
|
if (workerData.dataPoints.lastValue.usage !== 0)
|
|
workerData.dataPoints.push({time, usage: 0});
|
|
}
|
|
}
|
|
}
|
|
|
|
average /= visibleRecords.length;
|
|
mainThreadAverage /= visibleRecords.length;
|
|
webkitThreadAverage /= visibleRecords.length;
|
|
unknownThreadAverage /= visibleRecords.length;
|
|
|
|
for (let [workerId, workerData] of workersDataMap) {
|
|
workerData.average = workerData.average / workerData.recordsCount;
|
|
if (workerData.max > workerMax)
|
|
workerMax = workerData.max;
|
|
}
|
|
|
|
// 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 bestThreadLayoutMax(value) {
|
|
if (value > 100)
|
|
return Math.ceil(value);
|
|
return (Math.floor(value / 25) + 1) * 25;
|
|
}
|
|
|
|
function removeGreaterThan(arr, max) {
|
|
return arr.filter((x) => x <= max);
|
|
}
|
|
|
|
function markerValuesForMaxValue(max) {
|
|
if (max < 1)
|
|
return [0.5];
|
|
if (max < 7)
|
|
return removeGreaterThan([1, 3, 5], max);
|
|
if (max < 12.5)
|
|
return removeGreaterThan([5, 10], max);
|
|
if (max < 20)
|
|
return removeGreaterThan([5, 10, 15], max);
|
|
if (max < 30)
|
|
return removeGreaterThan([10, 20, 30], max);
|
|
if (max < 50)
|
|
return removeGreaterThan([15, 30, 45], max);
|
|
if (max < 100)
|
|
return removeGreaterThan([25, 50, 75], max);
|
|
if (max < 200)
|
|
return removeGreaterThan([50, 100, 150], max);
|
|
if (max >= 200) {
|
|
let hundreds = Math.floor(max / 100);
|
|
let even = (hundreds % 2) === 0;
|
|
if (even) {
|
|
let top = hundreds * 100;
|
|
let bottom = top / 2;
|
|
return [bottom, top];
|
|
}
|
|
let top = hundreds * 100;
|
|
let bottom = 100;
|
|
let mid = (top + bottom) / 2;
|
|
return [bottom, mid, top];
|
|
}
|
|
}
|
|
|
|
function layoutView(view, property, graphHeight, layoutMax, {dataPoints, min, max, average}) {
|
|
if (min === Infinity)
|
|
min = 0;
|
|
if (max === -Infinity)
|
|
max = 0;
|
|
if (layoutMax === -Infinity)
|
|
layoutMax = 0;
|
|
|
|
let isAllThreadsGraph = property === null;
|
|
|
|
let graphMax = layoutMax * 1.05;
|
|
|
|
function xScale(time) {
|
|
return (time - graphStartTime) / secondsPerPixel;
|
|
}
|
|
|
|
let size = new WI.Size(xScale(graphEndTime), graphHeight);
|
|
|
|
function yScale(value) {
|
|
return size.height - ((value / graphMax) * size.height);
|
|
}
|
|
|
|
view.updateChart(dataPoints, size, visibleEndTime, min, max, average, xScale, yScale, property);
|
|
|
|
let markersElement = view.chart.element.querySelector(".markers");
|
|
if (!markersElement) {
|
|
markersElement = view.chart.element.appendChild(document.createElement("div"));
|
|
markersElement.className = "markers";
|
|
}
|
|
markersElement.removeChildren();
|
|
|
|
let markerValues;
|
|
if (isAllThreadsGraph)
|
|
markerValues = markerValuesForMaxValue(max);
|
|
else {
|
|
const minimumMarkerTextHeight = 17;
|
|
let percentPerPixel = 1 / (graphHeight / layoutMax);
|
|
if (layoutMax < 5) {
|
|
let minimumDisplayablePercentByTwo = Math.ceil((minimumMarkerTextHeight * percentPerPixel) / 2) * 2;
|
|
markerValues = [Math.max(minimumDisplayablePercentByTwo, Math.floor(max))];
|
|
} else {
|
|
let minimumDisplayablePercentByFive = Math.ceil((minimumMarkerTextHeight * percentPerPixel) / 5) * 5;
|
|
markerValues = [Math.max(minimumDisplayablePercentByFive, Math.floor(max))];
|
|
}
|
|
}
|
|
|
|
for (let value of markerValues) {
|
|
let marginTop = yScale(value);
|
|
|
|
let markerElement = markersElement.appendChild(document.createElement("div"));
|
|
markerElement.style.marginTop = marginTop.toFixed(2) + "px";
|
|
|
|
let labelElement = markerElement.appendChild(document.createElement("span"));
|
|
labelElement.classList.add("label");
|
|
const precision = 0;
|
|
labelElement.innerText = Number.percentageString(value / 100, precision);
|
|
}
|
|
}
|
|
|
|
// Layout the combined graph to the maximum total CPU usage.
|
|
// Layout all the thread graphs to the same time scale, the maximum across threads / thread groups.
|
|
this._layoutMax = max;
|
|
this._threadLayoutMax = bestThreadLayoutMax(Math.max(mainThreadMax, webkitThreadMax, unknownThreadMax, workerMax));
|
|
|
|
layoutView(this._cpuUsageView, null, CPUTimelineView.cpuUsageViewHeight, this._layoutMax, {dataPoints, min, max, average});
|
|
|
|
if (this._threadsDetailsElement.open) {
|
|
layoutView(this._mainThreadUsageView, "mainThreadUsage", CPUTimelineView.threadCPUUsageViewHeight, this._threadLayoutMax, {dataPoints, min: mainThreadMin, max: mainThreadMax, average: mainThreadAverage});
|
|
layoutView(this._webkitThreadUsageView, "webkitThreadUsage", CPUTimelineView.threadCPUUsageViewHeight, this._threadLayoutMax, {dataPoints, min: webkitThreadMin, max: webkitThreadMax, average: webkitThreadAverage});
|
|
layoutView(this._unknownThreadUsageView, "unknownThreadUsage", CPUTimelineView.threadCPUUsageViewHeight, this._threadLayoutMax, {dataPoints, min: unknownThreadMin, max: unknownThreadMax, average: unknownThreadAverage});
|
|
|
|
this._removeWorkerThreadViews();
|
|
|
|
for (let [workerId, workerData] of workersDataMap) {
|
|
let worker = WI.targetManager.targetForIdentifier(workerId);
|
|
let displayName = worker ? worker.displayName : WI.UIString("Worker Thread");
|
|
let workerView = new WI.CPUUsageView(displayName);
|
|
workerView.element.classList.add("worker-thread");
|
|
workerView.__workerId = workerId;
|
|
this.addSubview(workerView);
|
|
this._threadsDetailsElement.insertBefore(workerView.element, this._webkitThreadUsageView.element);
|
|
this._workerViews.push(workerView);
|
|
|
|
layoutView(workerView, "usage", CPUTimelineView.threadCPUUsageViewHeight, this._threadLayoutMax, {dataPoints: workerData.dataPoints, min: workerData.min, max: workerData.max, average: workerData.average});
|
|
}
|
|
}
|
|
|
|
function xScaleIndicatorRange(sampleIndex) {
|
|
return (sampleIndex / 1000) / secondsPerPixel;
|
|
}
|
|
|
|
let graphWidth = (graphEndTime - graphStartTime) / secondsPerPixel;
|
|
let size = new WI.Size(graphWidth, CPUTimelineView.indicatorViewHeight);
|
|
this._cpuUsageView.updateMainThreadIndicator(this._statisticsData.samples, size, visibleEndTime, xScaleIndicatorRange);
|
|
|
|
this._layoutEnergyChart(average, visibleDuration);
|
|
|
|
this._updateGraphOverlay();
|
|
}
|
|
|
|
// Private
|
|
|
|
_layoutBreakdownChart()
|
|
{
|
|
let {samples, samplesScript, samplesLayout, samplesPaint, samplesStyle, samplesIdle} = this._statisticsData;
|
|
|
|
let nonIdleSamplesCount = samples.length - samplesIdle;
|
|
if (!nonIdleSamplesCount) {
|
|
this._breakdownChart.clear();
|
|
this._breakdownChart.needsLayout();
|
|
this._clearBreakdownLegend();
|
|
return;
|
|
}
|
|
|
|
let percentScript = samplesScript / nonIdleSamplesCount;
|
|
let percentLayout = samplesLayout / nonIdleSamplesCount;
|
|
let percentPaint = samplesPaint / nonIdleSamplesCount;
|
|
let percentStyle = samplesStyle / nonIdleSamplesCount;
|
|
|
|
this._breakdownLegendScriptElement.textContent = `${Number.percentageString(percentScript)} (${samplesScript})`;
|
|
this._breakdownLegendLayoutElement.textContent = `${Number.percentageString(percentLayout)} (${samplesLayout})`;
|
|
this._breakdownLegendPaintElement.textContent = `${Number.percentageString(percentPaint)} (${samplesPaint})`;
|
|
this._breakdownLegendStyleElement.textContent = `${Number.percentageString(percentStyle)} (${samplesStyle})`;
|
|
|
|
this._breakdownChart.values = [percentScript * 100, percentLayout * 100, percentPaint * 100, percentStyle * 100];
|
|
this._breakdownChart.needsLayout();
|
|
|
|
let centerElement = this._breakdownChart.centerElement;
|
|
let samplesElement = centerElement.firstChild;
|
|
if (!samplesElement) {
|
|
samplesElement = centerElement.appendChild(document.createElement("div"));
|
|
samplesElement.classList.add("samples");
|
|
samplesElement.title = WI.UIString("Time spent on the main thread");
|
|
}
|
|
|
|
let millisecondsStringNoDecimal = WI.UIString("%.0fms").format(nonIdleSamplesCount);
|
|
samplesElement.textContent = millisecondsStringNoDecimal;
|
|
}
|
|
|
|
_layoutStatisticsAndSources()
|
|
{
|
|
this._layoutStatisticsSection();
|
|
this._layoutSourcesSection();
|
|
}
|
|
|
|
_layoutStatisticsSection()
|
|
{
|
|
let statistics = this._statisticsData;
|
|
|
|
this._clearStatistics();
|
|
|
|
this._networkRequestsNumberElement.textContent = statistics.networkRequests;
|
|
this._scriptEntriesNumberElement.textContent = statistics.scriptEntries;
|
|
|
|
let createFilterElement = (type, name) => {
|
|
let span = document.createElement("span");
|
|
span.className = "filter";
|
|
span.textContent = name;
|
|
span.addEventListener("mouseup", (event) => {
|
|
if (span.classList.contains("active"))
|
|
this._removeSourcesFilter(type, name);
|
|
else
|
|
this._addSourcesFilter(type, name);
|
|
|
|
this._layoutStatisticsAndSources();
|
|
});
|
|
|
|
span.classList.toggle("active", this._sourcesFilter[type].has(name));
|
|
|
|
return span;
|
|
};
|
|
|
|
let expandAllSections = () => {
|
|
this._sectionLimit = Infinity;
|
|
this._layoutStatisticsAndSources();
|
|
};
|
|
|
|
function createEllipsisElement() {
|
|
let span = document.createElement("span");
|
|
span.className = "show-more";
|
|
span.role = "button";
|
|
span.textContent = ellipsis;
|
|
span.addEventListener("click", (event) => {
|
|
expandAllSections();
|
|
});
|
|
return span;
|
|
}
|
|
|
|
// Sort a Map of key => count values in descending order.
|
|
function sortMapByEntryCount(map) {
|
|
let entries = Array.from(map);
|
|
entries.sort((entryA, entryB) => entryB[1] - entryA[1]);
|
|
return new Map(entries);
|
|
}
|
|
|
|
if (statistics.timerTypes.size) {
|
|
let i = 0;
|
|
let sorted = sortMapByEntryCount(statistics.timerTypes);
|
|
for (let [timerType, count] of sorted) {
|
|
let headerValue = i === 0 ? WI.UIString("Timers:") : "";
|
|
let timerTypeElement = createFilterElement("timer", timerType);
|
|
this._insertTableRow(this._statisticsTable, this._statisticsRows, {headerValue, numberValue: count, labelValue: timerTypeElement});
|
|
|
|
if (++i === this._sectionLimit && sorted.size > this._sectionLimit) {
|
|
this._insertTableRow(this._statisticsTable, this._statisticsRows, {labelValue: createEllipsisElement()});
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (statistics.eventTypes.size) {
|
|
let i = 0;
|
|
let sorted = sortMapByEntryCount(statistics.eventTypes);
|
|
for (let [eventType, count] of sorted) {
|
|
let headerValue = i === 0 ? WI.UIString("Events:") : "";
|
|
let eventTypeElement = createFilterElement("event", eventType);
|
|
this._insertTableRow(this._statisticsTable, this._statisticsRows, {headerValue, numberValue: count, labelValue: eventTypeElement});
|
|
|
|
if (++i === this._sectionLimit && sorted.size > this._sectionLimit) {
|
|
this._insertTableRow(this._statisticsTable, this._statisticsRows, {labelValue: createEllipsisElement()});
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (statistics.observerTypes.size) {
|
|
let i = 0;
|
|
let sorted = sortMapByEntryCount(statistics.observerTypes);
|
|
for (let [observerType, count] of sorted) {
|
|
let headerValue = i === 0 ? WI.UIString("Observers:") : "";
|
|
let observerTypeElement = createFilterElement("observer", observerType);
|
|
this._insertTableRow(this._statisticsTable, this._statisticsRows, {headerValue, numberValue: count, labelValue: observerTypeElement});
|
|
|
|
if (++i === this._sectionLimit && sorted.size > this._sectionLimit) {
|
|
this._insertTableRow(this._statisticsTable, this._statisticsRows, {labelValue: createEllipsisElement()});
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_layoutSourcesSection()
|
|
{
|
|
let statistics = this._statisticsData;
|
|
|
|
this._clearSources();
|
|
|
|
const unknownLocationKey = "unknown";
|
|
|
|
function keyForSourceCodeLocation(sourceCodeLocation) {
|
|
if (!sourceCodeLocation)
|
|
return unknownLocationKey;
|
|
|
|
return sourceCodeLocation.sourceCode.url + ":" + sourceCodeLocation.lineNumber + ":" + sourceCodeLocation.columnNumber;
|
|
}
|
|
|
|
function labelForLocation(key, sourceCodeLocation, functionName) {
|
|
if (key === unknownLocationKey) {
|
|
let span = document.createElement("span");
|
|
span.className = "unknown";
|
|
span.textContent = WI.UIString("Unknown Location");
|
|
return span;
|
|
}
|
|
|
|
const options = {
|
|
nameStyle: WI.SourceCodeLocation.NameStyle.Short,
|
|
columnStyle: WI.SourceCodeLocation.ColumnStyle.Shown,
|
|
dontFloat: true,
|
|
ignoreNetworkTab: true,
|
|
ignoreSearchTab: true,
|
|
};
|
|
return WI.createSourceCodeLocationLink(sourceCodeLocation, options);
|
|
}
|
|
|
|
let timerFilters = this._sourcesFilter.timer;
|
|
let eventFilters = this._sourcesFilter.event;
|
|
let observerFilters = this._sourcesFilter.observer;
|
|
let hasFilters = (timerFilters.size || eventFilters.size || observerFilters.size);
|
|
|
|
let sectionLimit = this._sectionLimit;
|
|
if (isFinite(sectionLimit) && hasFilters)
|
|
sectionLimit = CPUTimelineView.defaultSectionLimit * 2;
|
|
|
|
let expandAllSections = () => {
|
|
this._sectionLimit = Infinity;
|
|
this._layoutStatisticsAndSources();
|
|
};
|
|
|
|
function createEllipsisElement() {
|
|
let span = document.createElement("span");
|
|
span.className = "show-more";
|
|
span.role = "button";
|
|
span.textContent = ellipsis;
|
|
span.addEventListener("click", (event) => {
|
|
expandAllSections();
|
|
});
|
|
return span;
|
|
}
|
|
|
|
let timerMap = new Map;
|
|
let eventHandlerMap = new Map;
|
|
let observerCallbackMap = new Map;
|
|
let seenTimers = new Set;
|
|
|
|
if (!hasFilters || timerFilters.size) {
|
|
// Aggregate timers on the location where the timers were installed.
|
|
// For repeating timers, this includes the total counts the interval fired in the selected time range.
|
|
for (let record of statistics.timerInstallationRecords) {
|
|
if (timerFilters.size) {
|
|
if (record.eventType === WI.ScriptTimelineRecord.EventType.AnimationFrameRequested && !timerFilters.has("requestAnimationFrame"))
|
|
continue;
|
|
if (record.eventType === WI.ScriptTimelineRecord.EventType.TimerInstalled && !timerFilters.has("setTimeout"))
|
|
continue;
|
|
}
|
|
|
|
let callFrame = record.initiatorCallFrame;
|
|
let sourceCodeLocation = callFrame ? callFrame.sourceCodeLocation : record.sourceCodeLocation;
|
|
let functionName = callFrame ? callFrame.functionName : "";
|
|
let key = keyForSourceCodeLocation(sourceCodeLocation);
|
|
let entry = timerMap.getOrInitialize(key, {sourceCodeLocation, functionName, count: 0, repeating: false});
|
|
if (record.details) {
|
|
let timerIdentifier = record.details.timerId;
|
|
let repeatingEntry = statistics.repeatingTimers.get(timerIdentifier);
|
|
let count = repeatingEntry ? repeatingEntry.count : 1;
|
|
entry.count += count;
|
|
if (record.details.repeating)
|
|
entry.repeating = true;
|
|
seenTimers.add(timerIdentifier);
|
|
} else
|
|
entry.count += 1;
|
|
}
|
|
|
|
// Aggregate repeating timers where we did not see the installation in the selected time range.
|
|
// This will use the source code location of where the timer fired, which is better than nothing.
|
|
if (!hasFilters || timerFilters.has("setTimeout")) {
|
|
for (let [timerId, repeatingEntry] of statistics.repeatingTimers) {
|
|
if (seenTimers.has(timerId))
|
|
continue;
|
|
// FIXME: <https://webkit.org/b/195351> Web Inspector: CPU Usage Timeline - better resolution of installation source for repeated timers
|
|
// We could have a map of all repeating timer installations in the whole recording
|
|
// so that we can provide a function name for these repeating timers lacking an installation point.
|
|
let sourceCodeLocation = repeatingEntry.record.sourceCodeLocation;
|
|
let key = keyForSourceCodeLocation(sourceCodeLocation);
|
|
let entry = timerMap.getOrInitialize(key, {sourceCodeLocation, count: 0, repeating: false});
|
|
entry.count += repeatingEntry.count;
|
|
entry.repeating = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!hasFilters || eventFilters.size) {
|
|
for (let record of statistics.eventHandlerRecords) {
|
|
if (eventFilters.size && !eventFilters.has(record.details))
|
|
continue;
|
|
let sourceCodeLocation = record.sourceCodeLocation;
|
|
let key = keyForSourceCodeLocation(sourceCodeLocation);
|
|
let entry = eventHandlerMap.getOrInitialize(key, {sourceCodeLocation, count: 0});
|
|
entry.count += 1;
|
|
}
|
|
}
|
|
|
|
if (!hasFilters || observerFilters.size) {
|
|
for (let record of statistics.observerCallbackRecords) {
|
|
if (observerFilters.size && !observerFilters.has(record.details))
|
|
continue;
|
|
let sourceCodeLocation = record.sourceCodeLocation;
|
|
let key = keyForSourceCodeLocation(record.sourceCodeLocation);
|
|
let entry = observerCallbackMap.getOrInitialize(key, {sourceCodeLocation, count: 0});
|
|
entry.count += 1;
|
|
}
|
|
}
|
|
|
|
const headerValue = "";
|
|
|
|
// Sort a Map of key => {count} objects in descending order.
|
|
function sortMapByEntryCountProperty(map) {
|
|
let entries = Array.from(map);
|
|
entries.sort((entryA, entryB) => entryB[1].count - entryA[1].count);
|
|
return new Map(entries);
|
|
}
|
|
|
|
if (timerMap.size) {
|
|
let i = 0;
|
|
let sorted = sortMapByEntryCountProperty(timerMap);
|
|
for (let [key, entry] of sorted) {
|
|
let numberValue = entry.repeating ? WI.UIString("~%s", "Approximate Number", "Approximate count of events").format(entry.count) : entry.count;
|
|
let sourceCodeLocation = entry.callFrame ? entry.callFrame.sourceCodeLocation : entry.sourceCodeLocation;
|
|
let labelValue = labelForLocation(key, sourceCodeLocation);
|
|
let followingRow = this._eventHandlersRow;
|
|
|
|
let row;
|
|
if (i === 0) {
|
|
row = this._timerInstallationsRow;
|
|
this._timerInstallationsNumberElement.textContent = numberValue;
|
|
this._timerInstallationsLabelElement.append(labelValue);
|
|
} else
|
|
row = this._insertTableRow(this._sourcesTable, this._sourcesRows, {headerValue, numberValue, labelValue, followingRow});
|
|
|
|
if (entry.functionName)
|
|
row.querySelector(".label").append(` ${enDash} ${entry.functionName}`);
|
|
|
|
if (++i === sectionLimit && sorted.size > sectionLimit) {
|
|
this._insertTableRow(this._sourcesTable, this._sourcesRows, {labelValue: createEllipsisElement(), followingRow});
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (eventHandlerMap.size) {
|
|
let i = 0;
|
|
let sorted = sortMapByEntryCountProperty(eventHandlerMap);
|
|
for (let [key, entry] of sorted) {
|
|
let numberValue = entry.count;
|
|
let labelValue = labelForLocation(key, entry.sourceCodeLocation);
|
|
let followingRow = this._observerHandlersRow;
|
|
|
|
if (i === 0) {
|
|
this._eventHandlersNumberElement.textContent = numberValue;
|
|
this._eventHandlersLabelElement.append(labelValue);
|
|
} else
|
|
this._insertTableRow(this._sourcesTable, this._sourcesRows, {headerValue, numberValue, labelValue, followingRow});
|
|
|
|
if (++i === sectionLimit && sorted.size > sectionLimit) {
|
|
this._insertTableRow(this._sourcesTable, this._sourcesRows, {labelValue: createEllipsisElement(), followingRow});
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (observerCallbackMap.size) {
|
|
let i = 0;
|
|
let sorted = sortMapByEntryCountProperty(observerCallbackMap);
|
|
for (let [key, entry] of sorted) {
|
|
let numberValue = entry.count;
|
|
let labelValue = labelForLocation(key, entry.sourceCodeLocation);
|
|
|
|
if (i === 0) {
|
|
this._observerHandlersNumberElement.textContent = numberValue;
|
|
this._observerHandlersLabelElement.append(labelValue);
|
|
} else
|
|
this._insertTableRow(this._sourcesTable, this._sourcesRows, {headerValue, numberValue, labelValue});
|
|
|
|
if (++i === sectionLimit && sorted.size > sectionLimit) {
|
|
this._insertTableRow(this._sourcesTable, this._sourcesRows, {labelValue: createEllipsisElement()});
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_layoutEnergyChart(average, visibleDuration)
|
|
{
|
|
// The lower the bias value [0..1], the more it increases the skew towards rangeHigh.
|
|
function mapWithBias(value, rangeLow, rangeHigh, outputRangeLow, outputRangeHigh, bias) {
|
|
console.assert(value >= rangeLow && value <= rangeHigh, "value was not in range.", value);
|
|
let percentInRange = (value - rangeLow) / (rangeHigh - rangeLow);
|
|
let skewedPercent = Math.pow(percentInRange, bias);
|
|
let valueInOutputRange = (skewedPercent * (outputRangeHigh - outputRangeLow)) + outputRangeLow;
|
|
return valueInOutputRange;
|
|
}
|
|
|
|
this._clearEnergyImpactText();
|
|
|
|
if (average <= CPUTimelineView.lowEnergyThreshold) {
|
|
// Low. (<=3% CPU, mapped to 0-10)
|
|
this._energyImpactLabelElement.textContent = WI.UIString("Low", "Low @ Timeline Energy Impact", "Energy Impact: Low");
|
|
this._energyImpactLabelElement.classList.add("low");
|
|
this._energyChart.value = mapWithBias(average, 0, CPUTimelineView.lowEnergyThreshold, 0, CPUTimelineView.lowEnergyGraphBoundary, 0.85);
|
|
} else if (average <= CPUTimelineView. mediumEnergyThreshold) {
|
|
// Medium (3%-30% CPU, mapped to 10-70)
|
|
this._energyImpactLabelElement.textContent = WI.UIString("Medium", "Medium @ Timeline Energy Impact", "Energy Impact: Medium")
|
|
this._energyImpactLabelElement.classList.add("medium");
|
|
this._energyChart.value = mapWithBias(average, CPUTimelineView.lowEnergyThreshold, CPUTimelineView.mediumEnergyThreshold, CPUTimelineView.lowEnergyGraphBoundary, CPUTimelineView.mediumEnergyGraphBoundary, 0.6);
|
|
} else if (average < CPUTimelineView. highEnergyThreshold) {
|
|
// High. (30%-100% CPU, mapped to 70-100)
|
|
this._energyImpactLabelElement.textContent = WI.UIString("High", "High @ Timeline Energy Impact", "Energy Impact: High")
|
|
this._energyImpactLabelElement.classList.add("high");
|
|
this._energyChart.value = mapWithBias(average, CPUTimelineView.mediumEnergyThreshold, CPUTimelineView.highEnergyThreshold, CPUTimelineView.mediumEnergyGraphBoundary, CPUTimelineView.highEnergyGraphBoundary, 0.9);
|
|
} else {
|
|
// Very High. (>100% CPU, mapped to 100)
|
|
this._energyImpactLabelElement.textContent = WI.UIString("Very High", "Very High @ Timeline Energy Impact", "Energy Impact: Very High")
|
|
this._energyImpactLabelElement.classList.add("high");
|
|
this._energyChart.value = 100;
|
|
}
|
|
|
|
this._energyChart.needsLayout();
|
|
|
|
this._energyImpactNumberElement.textContent = WI.UIString("Average CPU: %s").format(Number.percentageString(average / 100));
|
|
|
|
if (visibleDuration < 5)
|
|
this._energyImpactDurationElement.textContent = WI.UIString("Duration: Short");
|
|
else {
|
|
let durationDisplayString = Math.floor(visibleDuration);
|
|
this._energyImpactDurationElement.textContent = WI.UIString("Duration: %ss", "The duration of the Timeline recording in seconds (s).").format(durationDisplayString);
|
|
}
|
|
}
|
|
|
|
_computeStatisticsData(startTime, endTime)
|
|
{
|
|
// Compute per-millisecond samples of what the main thread was doing.
|
|
// We construct an array for every millisecond between the start and end time
|
|
// and mark each millisecond with the best representation of the work that
|
|
// was being done at that time. We start by populating the samples with
|
|
// all of the script periods and then override with layout and rendering
|
|
// samples. This means a forced layout would be counted as a layout:
|
|
//
|
|
// Initial: [ ------, ------, ------, ------, ------ ]
|
|
// Script Samples: [ ------, Script, Script, Script, ------ ]
|
|
// Layout Samples: [ ------, Script, Layout, Script, ------ ]
|
|
//
|
|
// The undefined samples are considered Idle, but in actuality WebKit
|
|
// may have been doing some work (such as hit testing / inspector protocol)
|
|
// that is not included it in generic Timeline data. This just works with
|
|
// with the data available to the frontend and is quite accurate for most
|
|
// Main Thread activity.
|
|
|
|
function incrementTypeCount(map, key) {
|
|
let entry = map.get(key);
|
|
if (entry)
|
|
map.set(key, entry + 1);
|
|
else
|
|
map.set(key, 1);
|
|
}
|
|
|
|
let timerInstallationRecords = [];
|
|
let eventHandlerRecords = [];
|
|
let observerCallbackRecords = [];
|
|
let scriptEntries = 0;
|
|
let timerTypes = new Map;
|
|
let eventTypes = new Map;
|
|
let observerTypes = new Map;
|
|
|
|
let repeatingTimers = new Map;
|
|
let possibleRepeatingTimers = new Set;
|
|
|
|
let scriptTimeline = this._recording.timelineForRecordType(WI.TimelineRecord.Type.Script);
|
|
let scriptRecords = scriptTimeline ? scriptTimeline.recordsInTimeRange(startTime, endTime, {includeRecordBeforeStart: true}) : [];
|
|
scriptRecords = scriptRecords.filter((record) => {
|
|
// Return true for event types that define script entries/exits.
|
|
// Return false for events with no time ranges or if they are contained in other events.
|
|
switch (record.eventType) {
|
|
case WI.ScriptTimelineRecord.EventType.ScriptEvaluated:
|
|
case WI.ScriptTimelineRecord.EventType.APIScriptEvaluated:
|
|
scriptEntries++;
|
|
return true;
|
|
|
|
case WI.ScriptTimelineRecord.EventType.ObserverCallback:
|
|
incrementTypeCount(observerTypes, record.details);
|
|
observerCallbackRecords.push(record);
|
|
scriptEntries++;
|
|
return true;
|
|
|
|
case WI.ScriptTimelineRecord.EventType.EventDispatched:
|
|
incrementTypeCount(eventTypes, record.details);
|
|
eventHandlerRecords.push(record);
|
|
scriptEntries++;
|
|
return true;
|
|
|
|
case WI.ScriptTimelineRecord.EventType.MicrotaskDispatched:
|
|
// Do not normally count this as a script entry, but they may have a time range
|
|
// that is not covered by script entry (queueMicrotask).
|
|
return true;
|
|
|
|
case WI.ScriptTimelineRecord.EventType.TimerFired:
|
|
incrementTypeCount(timerTypes, "setTimeout");
|
|
if (possibleRepeatingTimers.has(record.details)) {
|
|
let entry = repeatingTimers.get(record.details);
|
|
if (entry)
|
|
entry.count += 1;
|
|
else
|
|
repeatingTimers.set(record.details, {record, count: 1});
|
|
} else
|
|
possibleRepeatingTimers.add(record.details);
|
|
scriptEntries++;
|
|
return true;
|
|
|
|
case WI.ScriptTimelineRecord.EventType.AnimationFrameFired:
|
|
incrementTypeCount(timerTypes, "requestAnimationFrame");
|
|
scriptEntries++;
|
|
return true;
|
|
|
|
case WI.ScriptTimelineRecord.EventType.AnimationFrameRequested:
|
|
case WI.ScriptTimelineRecord.EventType.TimerInstalled:
|
|
// These event types have no time range, or are contained by the others.
|
|
timerInstallationRecords.push(record);
|
|
return false;
|
|
|
|
case WI.ScriptTimelineRecord.EventType.AnimationFrameCanceled:
|
|
case WI.ScriptTimelineRecord.EventType.TimerRemoved:
|
|
case WI.ScriptTimelineRecord.EventType.ProbeSampleRecorded:
|
|
case WI.ScriptTimelineRecord.EventType.ConsoleProfileRecorded:
|
|
case WI.ScriptTimelineRecord.EventType.GarbageCollected:
|
|
// These event types have no time range, or are contained by the others.
|
|
return false;
|
|
|
|
default:
|
|
console.error("Unhandled ScriptTimelineRecord.EventType", record.eventType);
|
|
return false;
|
|
}
|
|
});
|
|
|
|
let layoutTimeline = this._recording.timelineForRecordType(WI.TimelineRecord.Type.Layout);
|
|
let layoutRecords = layoutTimeline ? layoutTimeline.recordsInTimeRange(startTime, endTime, {includeRecordBeforeStart: true}) : [];
|
|
layoutRecords = layoutRecords.filter((record) => {
|
|
switch (record.eventType) {
|
|
case WI.LayoutTimelineRecord.EventType.RecalculateStyles:
|
|
case WI.LayoutTimelineRecord.EventType.ForcedLayout:
|
|
case WI.LayoutTimelineRecord.EventType.Layout:
|
|
case WI.LayoutTimelineRecord.EventType.Paint:
|
|
case WI.LayoutTimelineRecord.EventType.Composite:
|
|
// These event types define layout and rendering entry/exits.
|
|
return true;
|
|
|
|
case WI.LayoutTimelineRecord.EventType.InvalidateStyles:
|
|
case WI.LayoutTimelineRecord.EventType.InvalidateLayout:
|
|
// These event types have no time range.
|
|
return false;
|
|
|
|
default:
|
|
console.error("Unhandled LayoutTimelineRecord.EventType", record.eventType);
|
|
return false;
|
|
}
|
|
});
|
|
|
|
let networkTimeline = this._recording.timelineForRecordType(WI.TimelineRecord.Type.Network);
|
|
let networkRecords = networkTimeline ? networkTimeline.recordsInTimeRange(startTime, endTime) : [];
|
|
let networkRequests = networkRecords.length;
|
|
|
|
let millisecondStartTime = Math.round(startTime * 1000);
|
|
let millisecondEndTime = Math.round(endTime * 1000);
|
|
let millisecondDuration = millisecondEndTime - millisecondStartTime;
|
|
|
|
let samples = new Array(millisecondDuration);
|
|
|
|
function markRecordEntries(records, callback) {
|
|
for (let record of records) {
|
|
let recordStart = Math.round(record.startTime * 1000);
|
|
let recordEnd = Math.round(record.endTime * 1000);
|
|
if (recordStart > millisecondEndTime)
|
|
continue;
|
|
if (recordEnd < millisecondStartTime)
|
|
continue;
|
|
|
|
recordStart = Math.max(recordStart, millisecondStartTime);
|
|
recordEnd = Math.min(recordEnd, millisecondEndTime);
|
|
|
|
let value = callback(record);
|
|
for (let t = recordStart; t <= recordEnd; ++t)
|
|
samples[t - millisecondStartTime] = value;
|
|
}
|
|
}
|
|
|
|
markRecordEntries(scriptRecords, (record) => {
|
|
return CPUTimelineView.SampleType.JavaScript;
|
|
});
|
|
|
|
markRecordEntries(layoutRecords, (record) => {
|
|
switch (record.eventType) {
|
|
case WI.LayoutTimelineRecord.EventType.RecalculateStyles:
|
|
return CPUTimelineView.SampleType.Style;
|
|
case WI.LayoutTimelineRecord.EventType.ForcedLayout:
|
|
case WI.LayoutTimelineRecord.EventType.Layout:
|
|
return CPUTimelineView.SampleType.Layout;
|
|
case WI.LayoutTimelineRecord.EventType.Paint:
|
|
case WI.LayoutTimelineRecord.EventType.Composite:
|
|
return CPUTimelineView.SampleType.Paint;
|
|
}
|
|
});
|
|
|
|
let samplesIdle = 0;
|
|
let samplesScript = 0;
|
|
let samplesLayout = 0;
|
|
let samplesPaint = 0;
|
|
let samplesStyle = 0;
|
|
for (let i = 0; i < samples.length; ++i) {
|
|
switch (samples[i]) {
|
|
case undefined:
|
|
samplesIdle++;
|
|
break;
|
|
case CPUTimelineView.SampleType.JavaScript:
|
|
samplesScript++;
|
|
break;
|
|
case CPUTimelineView.SampleType.Layout:
|
|
samplesLayout++;
|
|
break;
|
|
case CPUTimelineView.SampleType.Paint:
|
|
samplesPaint++;
|
|
break;
|
|
case CPUTimelineView.SampleType.Style:
|
|
samplesStyle++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return {
|
|
samples,
|
|
samplesIdle,
|
|
samplesScript,
|
|
samplesLayout,
|
|
samplesPaint,
|
|
samplesStyle,
|
|
scriptEntries,
|
|
networkRequests,
|
|
timerTypes,
|
|
eventTypes,
|
|
observerTypes,
|
|
timerInstallationRecords,
|
|
eventHandlerRecords,
|
|
observerCallbackRecords,
|
|
repeatingTimers,
|
|
};
|
|
}
|
|
|
|
_removeWorkerThreadViews()
|
|
{
|
|
if (!this._workerViews.length)
|
|
return;
|
|
|
|
for (let view of this._workerViews)
|
|
this.removeSubview(view);
|
|
|
|
this._workerViews = [];
|
|
}
|
|
|
|
_resetSourcesFilters()
|
|
{
|
|
if (!this._sourcesFilter)
|
|
return;
|
|
|
|
this._sourcesFilterRow.hidden = true;
|
|
this._sourcesFilterLabelElement.removeChildren();
|
|
|
|
this._timerInstallationsRow.hidden = false;
|
|
this._eventHandlersRow.hidden = false;
|
|
this._observerHandlersRow.hidden = false;
|
|
|
|
this._sourcesFilter.timer.clear();
|
|
this._sourcesFilter.event.clear();
|
|
this._sourcesFilter.observer.clear();
|
|
}
|
|
|
|
_addSourcesFilter(type, name)
|
|
{
|
|
this._sourcesFilter[type].add(name);
|
|
this._updateSourcesFilters();
|
|
}
|
|
|
|
_removeSourcesFilter(type, name)
|
|
{
|
|
this._sourcesFilter[type].delete(name);
|
|
this._updateSourcesFilters();
|
|
}
|
|
|
|
_updateSourcesFilters()
|
|
{
|
|
let timerFilters = this._sourcesFilter.timer;
|
|
let eventFilters = this._sourcesFilter.event;
|
|
let observerFilters = this._sourcesFilter.observer;
|
|
|
|
if (!timerFilters.size && !eventFilters.size && !observerFilters.size) {
|
|
this._resetSourcesFilters();
|
|
return;
|
|
}
|
|
|
|
let createActiveFilterElement = (type, name) => {
|
|
let span = document.createElement("span");
|
|
span.className = "filter active";
|
|
span.textContent = name;
|
|
span.addEventListener("mouseup", (event) => {
|
|
this._removeSourcesFilter(type, name);
|
|
this._layoutStatisticsAndSources();
|
|
});
|
|
return span;
|
|
};
|
|
|
|
this._sourcesFilterRow.hidden = false;
|
|
this._sourcesFilterLabelElement.removeChildren();
|
|
|
|
for (let name of timerFilters)
|
|
this._sourcesFilterLabelElement.appendChild(createActiveFilterElement("timer", name));
|
|
for (let name of eventFilters)
|
|
this._sourcesFilterLabelElement.appendChild(createActiveFilterElement("event", name));
|
|
for (let name of observerFilters)
|
|
this._sourcesFilterLabelElement.appendChild(createActiveFilterElement("observer", name));
|
|
|
|
this._timerInstallationsRow.hidden = !timerFilters.size;
|
|
this._eventHandlersRow.hidden = !eventFilters.size;
|
|
this._observerHandlersRow.hidden = !observerFilters.size;
|
|
}
|
|
|
|
_createTableRow(table)
|
|
{
|
|
let row = table.appendChild(document.createElement("tr"));
|
|
|
|
let headerCell = row.appendChild(document.createElement("th"));
|
|
|
|
let numberCell = row.appendChild(document.createElement("td"));
|
|
numberCell.className = "number";
|
|
|
|
let labelCell = row.appendChild(document.createElement("td"));
|
|
labelCell.className = "label";
|
|
|
|
return {row, headerCell, numberCell, labelCell};
|
|
}
|
|
|
|
_insertTableRow(table, rowList, {headerValue, numberValue, labelValue, followingRow})
|
|
{
|
|
let {row, headerCell, numberCell, labelCell} = this._createTableRow(table);
|
|
rowList.push(row);
|
|
|
|
if (followingRow)
|
|
table.insertBefore(row, followingRow);
|
|
|
|
if (headerValue)
|
|
headerCell.textContent = headerValue;
|
|
|
|
if (numberValue)
|
|
numberCell.textContent = numberValue;
|
|
|
|
if (labelValue)
|
|
labelCell.append(labelValue);
|
|
|
|
return row;
|
|
}
|
|
|
|
_clearStatistics()
|
|
{
|
|
this._networkRequestsNumberElement.textContent = emDash;
|
|
this._scriptEntriesNumberElement.textContent = emDash;
|
|
|
|
for (let row of this._statisticsRows)
|
|
row.remove();
|
|
this._statisticsRows = [];
|
|
}
|
|
|
|
_clearSources()
|
|
{
|
|
this._timerInstallationsNumberElement.textContent = emDash;
|
|
this._timerInstallationsLabelElement.textContent = "";
|
|
|
|
this._eventHandlersNumberElement.textContent = emDash;
|
|
this._eventHandlersLabelElement.textContent = "";
|
|
|
|
this._observerHandlersNumberElement.textContent = emDash;
|
|
this._observerHandlersLabelElement.textContent = "";
|
|
|
|
for (let row of this._sourcesRows)
|
|
row.remove();
|
|
this._sourcesRows = [];
|
|
}
|
|
|
|
_clearEnergyImpactText()
|
|
{
|
|
this._energyImpactLabelElement.classList.remove("low", "medium", "high");
|
|
this._energyImpactLabelElement.textContent = emDash;
|
|
this._energyImpactNumberElement.textContent = "";
|
|
this._energyImpactDurationElement.textContent = "";
|
|
}
|
|
|
|
_clearBreakdownLegend()
|
|
{
|
|
this._breakdownLegendScriptElement.textContent = emDash;
|
|
this._breakdownLegendLayoutElement.textContent = emDash;
|
|
this._breakdownLegendPaintElement.textContent = emDash;
|
|
this._breakdownLegendStyleElement.textContent = emDash;
|
|
|
|
this._breakdownChart.centerElement.removeChildren();
|
|
}
|
|
|
|
_cpuTimelineRecordAdded(event)
|
|
{
|
|
let cpuTimelineRecord = event.data.record;
|
|
console.assert(cpuTimelineRecord instanceof WI.CPUTimelineRecord);
|
|
|
|
if (cpuTimelineRecord.startTime >= this.startTime && cpuTimelineRecord.endTime <= this.endTime)
|
|
this.needsLayout();
|
|
}
|
|
|
|
_graphPositionForMouseEvent(event)
|
|
{
|
|
let chartElement = event.target.closest(".area-chart, .stacked-area-chart, .range-chart");
|
|
if (!chartElement)
|
|
return NaN;
|
|
|
|
let rect = chartElement.getBoundingClientRect();
|
|
let position = event.pageX - rect.left;
|
|
|
|
if (WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL)
|
|
return rect.width - position;
|
|
return position;
|
|
}
|
|
|
|
_handleIndicatorClick(event)
|
|
{
|
|
let clickPosition = this._graphPositionForMouseEvent(event);
|
|
if (isNaN(clickPosition))
|
|
return;
|
|
|
|
let secondsPerPixel = this._timelineRuler.secondsPerPixel;
|
|
let graphClickTime = clickPosition * secondsPerPixel;
|
|
let graphStartTime = this.startTime;
|
|
|
|
let clickStartTime = graphStartTime + graphClickTime;
|
|
let clickEndTime = clickStartTime + secondsPerPixel;
|
|
|
|
// Try at the exact clicked pixel.
|
|
if (event.target.localName === "rect") {
|
|
if (this._attemptSelectIndicatatorTimelineRecord(clickStartTime, clickEndTime))
|
|
return;
|
|
console.assert(false, "If the user clicked on a rect there should have been a record in this pixel range");
|
|
}
|
|
|
|
// Spiral out 4 pixels each side to try and select a nearby record.
|
|
for (let i = 1, delta = 0; i <= 4; ++i) {
|
|
delta += secondsPerPixel;
|
|
if (this._attemptSelectIndicatatorTimelineRecord(clickStartTime - delta, clickStartTime))
|
|
return;
|
|
if (this._attemptSelectIndicatatorTimelineRecord(clickEndTime, clickEndTime + delta))
|
|
return;
|
|
}
|
|
}
|
|
|
|
_attemptSelectIndicatatorTimelineRecord(startTime, endTime)
|
|
{
|
|
let layoutTimeline = this._recording.timelineForRecordType(WI.TimelineRecord.Type.Layout);
|
|
let layoutRecords = layoutTimeline ? layoutTimeline.recordsInTimeRange(startTime, endTime, {includeRecordBeforeStart: true}) : [];
|
|
layoutRecords = layoutRecords.filter((record) => {
|
|
switch (record.eventType) {
|
|
case WI.LayoutTimelineRecord.EventType.RecalculateStyles:
|
|
case WI.LayoutTimelineRecord.EventType.ForcedLayout:
|
|
case WI.LayoutTimelineRecord.EventType.Layout:
|
|
case WI.LayoutTimelineRecord.EventType.Paint:
|
|
case WI.LayoutTimelineRecord.EventType.Composite:
|
|
return true;
|
|
case WI.LayoutTimelineRecord.EventType.InvalidateStyles:
|
|
case WI.LayoutTimelineRecord.EventType.InvalidateLayout:
|
|
return false;
|
|
default:
|
|
console.error("Unhandled LayoutTimelineRecord.EventType", record.eventType);
|
|
return false;
|
|
}
|
|
});
|
|
|
|
if (layoutRecords.length) {
|
|
this._selectTimelineRecord(layoutRecords[0]);
|
|
return true;
|
|
}
|
|
|
|
let scriptTimeline = this._recording.timelineForRecordType(WI.TimelineRecord.Type.Script);
|
|
let scriptRecords = scriptTimeline ? scriptTimeline.recordsInTimeRange(startTime, endTime, {includeRecordBeforeStart: true}) : [];
|
|
scriptRecords = scriptRecords.filter((record) => {
|
|
switch (record.eventType) {
|
|
case WI.ScriptTimelineRecord.EventType.ScriptEvaluated:
|
|
case WI.ScriptTimelineRecord.EventType.APIScriptEvaluated:
|
|
case WI.ScriptTimelineRecord.EventType.ObserverCallback:
|
|
case WI.ScriptTimelineRecord.EventType.EventDispatched:
|
|
case WI.ScriptTimelineRecord.EventType.MicrotaskDispatched:
|
|
case WI.ScriptTimelineRecord.EventType.TimerFired:
|
|
case WI.ScriptTimelineRecord.EventType.AnimationFrameFired:
|
|
return true;
|
|
case WI.ScriptTimelineRecord.EventType.AnimationFrameRequested:
|
|
case WI.ScriptTimelineRecord.EventType.AnimationFrameCanceled:
|
|
case WI.ScriptTimelineRecord.EventType.TimerInstalled:
|
|
case WI.ScriptTimelineRecord.EventType.TimerRemoved:
|
|
case WI.ScriptTimelineRecord.EventType.ProbeSampleRecorded:
|
|
case WI.ScriptTimelineRecord.EventType.ConsoleProfileRecorded:
|
|
case WI.ScriptTimelineRecord.EventType.GarbageCollected:
|
|
return false;
|
|
default:
|
|
console.error("Unhandled ScriptTimelineRecord.EventType", record.eventType);
|
|
return false;
|
|
}
|
|
});
|
|
|
|
if (scriptRecords.length) {
|
|
this._selectTimelineRecord(scriptRecords[0]);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
_selectTimelineRecord(record)
|
|
{
|
|
this.dispatchEventToListeners(WI.TimelineView.Event.RecordWasSelected, {record});
|
|
}
|
|
|
|
_handleGraphClick(event)
|
|
{
|
|
let mousePosition = this._graphPositionForMouseEvent(event);
|
|
if (isNaN(mousePosition))
|
|
return;
|
|
|
|
this._stickingOverlay = !this._stickingOverlay;
|
|
|
|
if (!this._stickingOverlay)
|
|
this._handleGraphMouseMove(event);
|
|
}
|
|
|
|
_handleGraphMouseMove(event)
|
|
{
|
|
let mousePosition = this._graphPositionForMouseEvent(event);
|
|
if (isNaN(mousePosition)) {
|
|
this._hideGraphOverlay();
|
|
this.dispatchEventToListeners(WI.TimelineView.Event.ScannerHide);
|
|
return;
|
|
}
|
|
|
|
let secondsPerPixel = this._timelineRuler.secondsPerPixel;
|
|
let time = this.startTime + (mousePosition * secondsPerPixel);
|
|
|
|
if (!this._stickingOverlay)
|
|
this._showGraphOverlayNearTo(time);
|
|
|
|
this.dispatchEventToListeners(WI.TimelineView.Event.ScannerShow, {time});
|
|
}
|
|
|
|
_showGraphOverlayNearTo(time)
|
|
{
|
|
let nearestRecord = null;
|
|
let nearestDistance = Infinity;
|
|
|
|
// Find the nearest record to the time.
|
|
for (let record of this._visibleRecordsInLayout) {
|
|
let distance = Math.abs(time - record.timestamp);
|
|
if (distance < nearestDistance) {
|
|
nearestRecord = record;
|
|
nearestDistance = distance;
|
|
}
|
|
}
|
|
|
|
if (!nearestRecord) {
|
|
this._hideGraphOverlay();
|
|
return;
|
|
}
|
|
|
|
let bestTime = nearestRecord.timestamp;
|
|
|
|
// Snap to a discontinuity if closer.
|
|
for (let {startTime, endTime} of this._discontinuitiesInLayout) {
|
|
let distance = Math.abs(time - startTime);
|
|
if (distance < nearestDistance) {
|
|
nearestDistance = distance;
|
|
bestTime = startTime;
|
|
}
|
|
distance = Math.abs(time - endTime);
|
|
if (distance < nearestDistance) {
|
|
nearestDistance = distance;
|
|
bestTime = endTime;
|
|
}
|
|
}
|
|
|
|
// Snap to end time if closer.
|
|
let visibleEndTime = Math.min(this.endTime, this.currentTime);
|
|
let distance = Math.abs(time - visibleEndTime);
|
|
if (distance < nearestDistance) {
|
|
nearestDistance = distance;
|
|
bestTime = visibleEndTime;
|
|
}
|
|
|
|
let graphStartTime = this.startTime;
|
|
let adjustedTime = Number.constrain(bestTime, graphStartTime, visibleEndTime);
|
|
this._showGraphOverlay(nearestRecord, adjustedTime);
|
|
}
|
|
|
|
_updateGraphOverlay()
|
|
{
|
|
if (!this._overlayRecord)
|
|
return;
|
|
|
|
this._showGraphOverlay(this._overlayRecord, this._overlayTime, true);
|
|
}
|
|
|
|
_showGraphOverlay(record, time, force)
|
|
{
|
|
if (!force && record === this._overlayRecord && time === this._overlayTime)
|
|
return;
|
|
|
|
this._overlayRecord = record;
|
|
this._overlayTime = time;
|
|
|
|
let secondsPerPixel = this._secondsPerPixelInLayout;
|
|
let graphStartTime = this.startTime;
|
|
|
|
this._overlayMarker.time = time + (secondsPerPixel / 2);
|
|
|
|
function xScale(time) {
|
|
return (time - graphStartTime) / secondsPerPixel;
|
|
}
|
|
|
|
let x = xScale(time);
|
|
|
|
let {mainThreadUsage, workerThreadUsage, webkitThreadUsage, unknownThreadUsage, workersData} = record;
|
|
|
|
function addOverlayPoint(view, graphHeight, layoutMax, value) {
|
|
if (!value)
|
|
return;
|
|
|
|
let graphMax = layoutMax * 1.05;
|
|
|
|
function yScale(value) {
|
|
return graphHeight - ((value / graphMax) * graphHeight);
|
|
}
|
|
|
|
view.chart.addPointMarker(x, yScale(value));
|
|
view.chart.needsLayout();
|
|
}
|
|
|
|
this._clearOverlayMarkers();
|
|
|
|
this._cpuUsageView.updateLegend(record);
|
|
addOverlayPoint(this._cpuUsageView, CPUTimelineView.cpuUsageViewHeight, this._layoutMax, mainThreadUsage);
|
|
addOverlayPoint(this._cpuUsageView, CPUTimelineView.cpuUsageViewHeight, this._layoutMax, mainThreadUsage + workerThreadUsage);
|
|
addOverlayPoint(this._cpuUsageView, CPUTimelineView.cpuUsageViewHeight, this._layoutMax, mainThreadUsage + workerThreadUsage + webkitThreadUsage + unknownThreadUsage);
|
|
|
|
if (this._threadsDetailsElement.open) {
|
|
this._mainThreadUsageView.updateLegend(mainThreadUsage);
|
|
addOverlayPoint(this._mainThreadUsageView, CPUTimelineView.threadCPUUsageViewHeight, this._threadLayoutMax, mainThreadUsage);
|
|
|
|
this._webkitThreadUsageView.updateLegend(webkitThreadUsage);
|
|
addOverlayPoint(this._webkitThreadUsageView, CPUTimelineView.threadCPUUsageViewHeight, this._threadLayoutMax, webkitThreadUsage);
|
|
|
|
this._unknownThreadUsageView.updateLegend(unknownThreadUsage);
|
|
addOverlayPoint(this._unknownThreadUsageView, CPUTimelineView.threadCPUUsageViewHeight, this._threadLayoutMax, unknownThreadUsage);
|
|
|
|
for (let workerView of this._workerViews)
|
|
workerView.updateLegend(NaN);
|
|
|
|
if (workersData) {
|
|
for (let {targetId, usage} of workersData) {
|
|
let workerView = this._workerViews.find((x) => x.__workerId === targetId);
|
|
if (workerView) {
|
|
workerView.updateLegend(usage);
|
|
addOverlayPoint(workerView, CPUTimelineView.threadCPUUsageViewHeight, this._threadLayoutMax, usage);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_clearOverlayMarkers()
|
|
{
|
|
function clearGraphOverlayElement(view) {
|
|
view.clearLegend();
|
|
view.chart.clearPointMarkers();
|
|
view.chart.needsLayout();
|
|
}
|
|
|
|
clearGraphOverlayElement(this._cpuUsageView);
|
|
clearGraphOverlayElement(this._mainThreadUsageView);
|
|
clearGraphOverlayElement(this._webkitThreadUsageView);
|
|
clearGraphOverlayElement(this._unknownThreadUsageView);
|
|
|
|
for (let workerView of this._workerViews)
|
|
clearGraphOverlayElement(workerView);
|
|
}
|
|
|
|
_hideGraphOverlay()
|
|
{
|
|
if (this._stickingOverlay)
|
|
return;
|
|
|
|
this._overlayRecord = null;
|
|
this._overlayTime = NaN;
|
|
this._overlayMarker.time = -1;
|
|
this._clearOverlayMarkers();
|
|
}
|
|
};
|
|
|
|
WI.CPUTimelineView.LayoutReason = {
|
|
Internal: Symbol("cpu-timeline-view-internal-layout"),
|
|
};
|
|
|
|
// NOTE: UI follows this order.
|
|
WI.CPUTimelineView.SampleType = {
|
|
JavaScript: "sample-type-javascript",
|
|
Layout: "sample-type-layout",
|
|
Paint: "sample-type-paint",
|
|
Style: "sample-type-style",
|
|
};
|
|
|
|
WI.CPUTimelineView.ReferencePage = WI.ReferencePage.TimelinesTab.CPUTimeline;
|