blob: e344a1ef5cb6b8616f5c78fec692f015efee9e6f [file] [log] [blame]
/*
* Copyright (C) 2013, 2015 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.
*/
WebInspector.TimelineSidebarPanel = class TimelineSidebarPanel extends WebInspector.NavigationSidebarPanel
{
constructor(contentBrowser)
{
super("timeline", WebInspector.UIString("Timelines"));
this.contentBrowser = contentBrowser;
this._timelineEventsTitleBarContainer = document.createElement("div");
this._timelineEventsTitleBarContainer.classList.add(WebInspector.TimelineSidebarPanel.TitleBarStyleClass);
this._timelineEventsTitleBarContainer.classList.add(WebInspector.TimelineSidebarPanel.TimelineEventsTitleBarStyleClass);
this._timelineEventsTitleBarElement = document.createElement("div");
this._timelineEventsTitleBarElement.classList.add(WebInspector.TimelineSidebarPanel.TitleBarTextStyleClass);
this._timelineEventsTitleBarContainer.appendChild(this._timelineEventsTitleBarElement);
this._timelineEventsTitleBarScopeContainer = document.createElement("div");
this._timelineEventsTitleBarScopeContainer.classList.add(WebInspector.TimelineSidebarPanel.TitleBarScopeBarStyleClass);
this._timelineEventsTitleBarContainer.appendChild(this._timelineEventsTitleBarScopeContainer);
this.element.insertBefore(this._timelineEventsTitleBarContainer, this.element.firstChild);
this.contentTreeOutlineLabel = "";
this._timelinesContentContainerElement = document.createElement("div");
this._timelinesContentContainerElement.classList.add(WebInspector.TimelineSidebarPanel.TimelinesContentContainerStyleClass);
this.element.insertBefore(this._timelinesContentContainerElement, this.element.firstChild);
this._displayedRecording = null;
this._displayedContentView = null;
this._viewMode = null;
this._previousSelectedTimelineType = null;
// Maintain an invisible tree outline containing tree elements for all recordings.
// The visible recording's tree element is selected when the content view changes.
this._recordingTreeElementMap = new Map;
this._recordingsTreeOutline = this.createContentTreeOutline(true, true);
this._recordingsTreeOutline.element.classList.add(WebInspector.NavigationSidebarPanel.HideDisclosureButtonsStyleClassName);
this._recordingsTreeOutline.element.classList.add(WebInspector.NavigationSidebarPanel.ContentTreeOutlineElementHiddenStyleClassName);
this._recordingsTreeOutline.addEventListener(WebInspector.TreeOutline.Event.SelectionDidChange, this._recordingsTreeSelectionDidChange, this);
this._timelinesContentContainerElement.appendChild(this._recordingsTreeOutline.element);
// Maintain a tree outline with tree elements for each timeline of the selected recording.
this._timelinesTreeOutline = this.createContentTreeOutline(true, true);
this._timelinesTreeOutline.element.classList.add(WebInspector.NavigationSidebarPanel.HideDisclosureButtonsStyleClassName);
this._timelinesTreeOutline.addEventListener(WebInspector.TreeOutline.Event.SelectionDidChange, this._timelinesTreeSelectionDidChange, this);
this._timelinesContentContainerElement.appendChild(this._timelinesTreeOutline.element);
this._timelineTreeElementMap = new Map;
this._basicTitleBar = document.createElement("div");
this._basicTitleBar.textContent = WebInspector.UIString("Timelines");
this._basicTitleBar.classList.add(WebInspector.TimelineSidebarPanel.TitleBarStyleClass);
this._basicTitleBar.classList.add(WebInspector.TimelineSidebarPanel.TimelinesTitleBarStyleClass);
this.element.insertBefore(this._basicTitleBar, this.element.firstChild);
if (WebInspector.FPSInstrument.supported()) {
var timelinesNavigationItem = new WebInspector.RadioButtonNavigationItem(WebInspector.TimelineSidebarPanel.ViewMode.Timelines, WebInspector.UIString("Timelines"))
var renderingFramesNavigationItem = new WebInspector.RadioButtonNavigationItem(WebInspector.TimelineSidebarPanel.ViewMode.RenderingFrames, WebInspector.UIString("Rendering Frames"))
this._viewModeNavigationBar = new WebInspector.NavigationBar(null, [timelinesNavigationItem, renderingFramesNavigationItem], "tablist");
this._viewModeNavigationBar.addEventListener(WebInspector.NavigationBar.Event.NavigationItemSelected, this._viewModeSelected, this);
this._renderingFramesTitleBar = document.createElement("div");
this._renderingFramesTitleBar.className = "navigation-bar-container";
this._renderingFramesTitleBar.appendChild(this._viewModeNavigationBar.element);
this._renderingFramesTitleBar.hidden = true;
this.element.insertBefore(this._renderingFramesTitleBar, this.element.firstChild);
this._chartColors = new Map;
this._chartColors.set(WebInspector.RenderingFrameTimelineRecord.TaskType.Script, "rgb(153, 113, 185)");
this._chartColors.set(WebInspector.RenderingFrameTimelineRecord.TaskType.Layout, "rgb(212, 108, 108)");
this._chartColors.set(WebInspector.RenderingFrameTimelineRecord.TaskType.Paint, "rgb(152, 188, 77)");
this._chartColors.set(WebInspector.RenderingFrameTimelineRecord.TaskType.Other, "rgb(221, 221, 221)");
this._frameSelectionChartRow = new WebInspector.ChartDetailsSectionRow(this, 74, 0.5);
this._frameSelectionChartRow.addEventListener(WebInspector.ChartDetailsSectionRow.Event.LegendItemChecked, this._frameSelectionLegendItemChecked, this);
for (let key in WebInspector.RenderingFrameTimelineRecord.TaskType) {
let taskType = WebInspector.RenderingFrameTimelineRecord.TaskType[key];
let label = WebInspector.RenderingFrameTimelineRecord.displayNameForTaskType(taskType);
let color = this._chartColors.get(taskType);
let checkbox = taskType !== WebInspector.RenderingFrameTimelineRecord.TaskType.Other;
this._frameSelectionChartRow.addItem(taskType, label, 0, color, checkbox, true);
}
this._renderingFrameTaskFilter = new Set;
var chartGroup = new WebInspector.DetailsSectionGroup([this._frameSelectionChartRow]);
this._frameSelectionChartSection = new WebInspector.DetailsSection("frames-selection-chart", WebInspector.UIString("Selected Frames"), [chartGroup], null, true);
this._timelinesContentContainerElement.appendChild(this._frameSelectionChartSection.element);
}
var statusBarElement = this._statusBarElement = document.createElement("div");
statusBarElement.classList.add(WebInspector.TimelineSidebarPanel.StatusBarStyleClass);
this.element.insertBefore(statusBarElement, this.element.firstChild);
this._recordGlyphElement = document.createElement("div");
this._recordGlyphElement.className = WebInspector.TimelineSidebarPanel.RecordGlyphStyleClass;
this._recordGlyphElement.title = WebInspector.UIString("Click or press the spacebar to record.")
this._recordGlyphElement.addEventListener("mouseover", this._recordGlyphMousedOver.bind(this));
this._recordGlyphElement.addEventListener("mouseout", this._recordGlyphMousedOut.bind(this));
this._recordGlyphElement.addEventListener("click", this._recordGlyphClicked.bind(this));
statusBarElement.appendChild(this._recordGlyphElement);
this._recordStatusElement = document.createElement("div");
this._recordStatusElement.className = WebInspector.TimelineSidebarPanel.RecordStatusStyleClass;
statusBarElement.appendChild(this._recordStatusElement);
WebInspector.showReplayInterfaceSetting.addEventListener(WebInspector.Setting.Event.Changed, this._updateReplayInterfaceVisibility, this);
// We always create a replay navigation bar; its visibility is controlled by WebInspector.showReplayInterfaceSetting.
this._replayNavigationBar = new WebInspector.NavigationBar;
this.element.appendChild(this._replayNavigationBar.element);
var toolTip = WebInspector.UIString("Begin Capturing");
var altToolTip = WebInspector.UIString("End Capturing");
this._replayCaptureButtonItem = new WebInspector.ActivateButtonNavigationItem("replay-capture", toolTip, altToolTip, "Images/Circle.svg", 16, 16);
this._replayCaptureButtonItem.addEventListener(WebInspector.ButtonNavigationItem.Event.Clicked, this._replayCaptureButtonClicked, this);
this._replayCaptureButtonItem.enabled = true;
this._replayNavigationBar.addNavigationItem(this._replayCaptureButtonItem);
toolTip = WebInspector.UIString("Start Playback");
altToolTip = WebInspector.UIString("Pause Playback");
this._replayPauseResumeButtonItem = new WebInspector.ToggleButtonNavigationItem("replay-pause-resume", toolTip, altToolTip, "Images/Resume.svg", "Images/Pause.svg", 15, 15);
this._replayPauseResumeButtonItem.addEventListener(WebInspector.ButtonNavigationItem.Event.Clicked, this._replayPauseResumeButtonClicked, this);
this._replayPauseResumeButtonItem.enabled = false;
this._replayNavigationBar.addNavigationItem(this._replayPauseResumeButtonItem);
WebInspector.replayManager.addEventListener(WebInspector.ReplayManager.Event.CaptureStarted, this._captureStarted, this);
WebInspector.replayManager.addEventListener(WebInspector.ReplayManager.Event.CaptureStopped, this._captureStopped, this);
this._statusBarElement.oncontextmenu = this._contextMenuNavigationBarOrStatusBar.bind(this);
this._replayNavigationBar.element.oncontextmenu = this._contextMenuNavigationBarOrStatusBar.bind(this);
this._updateReplayInterfaceVisibility();
WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.Event.RecordingCreated, this._recordingCreated, this);
WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.Event.RecordingLoaded, this._recordingLoaded, this);
this.contentBrowser.addEventListener(WebInspector.ContentBrowser.Event.CurrentContentViewDidChange, this._contentBrowserCurrentContentViewDidChange, this);
WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.Event.CapturingStarted, this._capturingStarted, this);
WebInspector.timelineManager.addEventListener(WebInspector.TimelineManager.Event.CapturingStopped, this._capturingStopped, this);
for (var recording of WebInspector.timelineManager.recordings)
this._addRecording(recording);
this._recordingCountChanged();
if (WebInspector.timelineManager.activeRecording)
this._recordingLoaded();
this._toggleRecordingShortcut = new WebInspector.KeyboardShortcut(null, WebInspector.KeyboardShortcut.Key.Space, this._toggleRecordingOnSpacebar.bind(this));
this._toggleRecordingShortcut.implicitlyPreventsDefault = false;
this._toggleNewRecordingShortcut = new WebInspector.KeyboardShortcut(WebInspector.KeyboardShortcut.Modifier.Shift, WebInspector.KeyboardShortcut.Key.Space, this._toggleNewRecordingOnSpacebar.bind(this));
this._toggleNewRecordingShortcut.implicitlyPreventsDefault = false;
}
// Static
static displayNameForTimeline(timeline)
{
switch (timeline.type) {
case WebInspector.TimelineRecord.Type.Network:
return WebInspector.UIString("Network Requests");
case WebInspector.TimelineRecord.Type.Layout:
return WebInspector.UIString("Layout & Rendering");
case WebInspector.TimelineRecord.Type.Script:
return WebInspector.UIString("JavaScript & Events");
case WebInspector.TimelineRecord.Type.RenderingFrame:
return WebInspector.UIString("Rendering Frames");
}
console.error("Unknown Timeline type:", timeline.type);
}
static iconClassNameForTimeline(timeline)
{
switch (timeline.type) {
case WebInspector.TimelineRecord.Type.Network:
return "network-icon";
case WebInspector.TimelineRecord.Type.Layout:
return "colors-icon";
case WebInspector.TimelineRecord.Type.Script:
return "script-icon";
case WebInspector.TimelineRecord.Type.RenderingFrame:
return "rendering-frame-icon";
}
console.error("Unknown Timeline type:", timeline.type);
}
// Public
shown()
{
super.shown();
if (this._displayedContentView)
this.contentBrowser.showContentView(this._displayedContentView);
if (this.viewMode === WebInspector.TimelineSidebarPanel.ViewMode.RenderingFrames)
this._refreshFrameSelectionChart();
this._toggleRecordingShortcut.disabled = false;
this._toggleNewRecordingShortcut.disabled = false;
this._updateTimelineOverviewHeight();
}
hidden()
{
super.hidden();
this._toggleRecordingShortcut.disabled = true;
this._toggleNewRecordingShortcut.disabled = true;
}
closed()
{
super.closed();
WebInspector.showReplayInterfaceSetting.removeEventListener(null, null, this);
WebInspector.replayManager.removeEventListener(null, null, this);
WebInspector.timelineManager.removeEventListener(null, null, this);
WebInspector.timelineManager.reset();
}
get viewMode()
{
return this._viewMode;
}
showDefaultContentView()
{
if (this._displayedContentView)
this.showTimelineOverview();
}
get hasSelectedElement()
{
return !!this._contentTreeOutline.selectedTreeElement || !!this._recordingsTreeOutline.selectedTreeElement;
}
treeElementForRepresentedObject(representedObject)
{
if (representedObject instanceof WebInspector.TimelineRecording)
return this._recordingTreeElementMap.get(representedObject);
// This fails if the timeline does not belong to the selected recording.
if (representedObject instanceof WebInspector.Timeline) {
var foundTreeElement = this._timelineTreeElementMap.get(representedObject);
if (foundTreeElement)
return foundTreeElement;
}
// The main resource is used as the representedObject instead of Frame in our tree.
if (representedObject instanceof WebInspector.Frame)
representedObject = representedObject.mainResource;
var foundTreeElement = this.contentTreeOutline.getCachedTreeElement(representedObject);
if (foundTreeElement)
return foundTreeElement;
// Look for TreeElements loosely based on represented objects that can contain the represented
// object we are really looking for. This allows a SourceCodeTimelineTreeElement or a
// TimelineRecordTreeElement to stay selected when the Resource it represents is showing.
function looselyCompareRepresentedObjects(candidateTreeElement)
{
if (!candidateTreeElement)
return false;
var candidateRepresentedObject = candidateTreeElement.representedObject;
if (candidateRepresentedObject instanceof WebInspector.SourceCodeTimeline) {
if (candidateRepresentedObject.sourceCode === representedObject)
return true;
return false;
} else if (candidateRepresentedObject instanceof WebInspector.Timeline && representedObject instanceof WebInspector.Timeline) {
// Reopen to the same timeline, even if a different parent recording is currently shown.
if (candidateRepresentedObject.type === representedObject.type)
return true;
return false;
}
if (candidateRepresentedObject instanceof WebInspector.TimelineRecord) {
if (!candidateRepresentedObject.sourceCodeLocation)
return false;
if (candidateRepresentedObject.sourceCodeLocation.sourceCode === representedObject)
return true;
return false;
}
if (candidateRepresentedObject instanceof WebInspector.ProfileNode)
return false;
console.error("Unknown TreeElement", candidateTreeElement);
return false;
}
// Check the selected tree element first so we don't need to do a longer search and it is
// likely to be the best candidate for the current view.
if (looselyCompareRepresentedObjects(this.contentTreeOutline.selectedTreeElement))
return this.contentTreeOutline.selectedTreeElement;
var currentTreeElement = this._contentTreeOutline.children[0];
while (currentTreeElement && !currentTreeElement.root) {
if (looselyCompareRepresentedObjects(currentTreeElement))
return currentTreeElement;
currentTreeElement = currentTreeElement.traverseNextTreeElement(false, null, false);
}
return null;
}
get contentTreeOutlineLabel()
{
return this._timelineEventsTitleBarElement.textContent;
}
set contentTreeOutlineLabel(label)
{
label = label || WebInspector.UIString("Timeline Events");
this._timelineEventsTitleBarElement.textContent = label;
this.filterBar.placeholder = WebInspector.UIString("Filter %s").format(label);
}
get contentTreeOutlineScopeBar()
{
return this._timelineEventsTitleBarScopeContainer.children;
}
set contentTreeOutlineScopeBar(scopeBar)
{
this._timelineEventsTitleBarScopeContainer.removeChildren();
if (!scopeBar || !scopeBar.element)
return;
this._timelineEventsTitleBarScopeContainer.appendChild(scopeBar.element);
}
showTimelineOverview()
{
if (this._timelinesTreeOutline.selectedTreeElement)
this._timelinesTreeOutline.selectedTreeElement.deselect();
this._displayedContentView.showOverviewTimelineView();
this.contentBrowser.showContentView(this._displayedContentView);
var selectedByUser = false;
this._changeViewMode(WebInspector.TimelineSidebarPanel.ViewMode.Timelines, selectedByUser);
}
showTimelineViewForTimeline(timeline)
{
console.assert(timeline instanceof WebInspector.Timeline, timeline);
console.assert(this._displayedRecording.timelines.has(timeline.type), "Cannot show timeline because it does not belong to the shown recording.", timeline.type);
// The sidebar view mode must be in the correct state before changing the content view.
var selectedByUser = false;
this._changeViewMode(this._viewModeForTimeline(timeline), selectedByUser);
if (this._timelineTreeElementMap.has(timeline)) {
// Defer showing the relevant timeline to the onselect handler of the timelines tree element.
var wasSelectedByUser = true;
var shouldSuppressOnSelect = false;
this._timelineTreeElementMap.get(timeline).select(true, wasSelectedByUser, shouldSuppressOnSelect, true);
} else {
this._displayedContentView.showTimelineViewForTimeline(timeline);
this.contentBrowser.showContentView(this._displayedContentView);
}
}
updateFrameSelection(startFrameIndex, endFrameIndex)
{
console.assert(startFrameIndex <= endFrameIndex, startFrameIndex, endFrameIndex);
console.assert(this.viewMode === WebInspector.TimelineSidebarPanel.ViewMode.RenderingFrames, this._viewMode);
if (this._startFrameIndex === startFrameIndex && this._endFrameIndex === endFrameIndex)
return;
this._startFrameIndex = startFrameIndex;
this._endFrameIndex = endFrameIndex;
this._refreshFrameSelectionChart();
}
formatChartValue(value)
{
return this._frameSelectionChartRow.total === 0 ? "" : Number.secondsToString(value);
}
// Protected
representedObjectWasFiltered(representedObject, filtered)
{
super.representedObjectWasFiltered(representedObject, filtered);
if (representedObject instanceof WebInspector.TimelineRecord)
this._displayedContentView.recordWasFiltered(representedObject, filtered);
}
updateFilter()
{
super.updateFilter();
this._displayedContentView.filterDidChange();
}
shouldFilterPopulate()
{
return false;
}
hasCustomFilters()
{
return true;
}
matchTreeElementAgainstCustomFilters(treeElement)
{
if (!this._displayedContentView)
return true;
if (this._viewMode === WebInspector.TimelineSidebarPanel.ViewMode.RenderingFrames && this._renderingFrameTaskFilter.size) {
while (treeElement && !(treeElement.record instanceof WebInspector.TimelineRecord))
treeElement = treeElement.parent;
console.assert(treeElement, "Cannot apply task filter: no TimelineRecord found.");
if (!treeElement)
return false;
let records;
if (treeElement.record instanceof WebInspector.RenderingFrameTimelineRecord)
records = treeElement.record.children;
else
records = [treeElement.record];
const filtered = records.every(function(record) {
var taskType = WebInspector.RenderingFrameTimelineRecord.taskTypeForTimelineRecord(record);
return this._renderingFrameTaskFilter.has(taskType);
}, this);
if (filtered)
return false;
}
return this._displayedContentView.matchTreeElementAgainstCustomFilters(treeElement);
}
treeElementAddedOrChanged(treeElement)
{
if (treeElement.status)
return;
if (!treeElement.treeOutline || typeof treeElement.treeOutline.__canShowContentViewForTreeElement !== "function")
return;
if (!treeElement.treeOutline.__canShowContentViewForTreeElement(treeElement))
return;
var fragment = document.createDocumentFragment();
var closeButton = new WebInspector.TreeElementStatusButton(useSVGSymbol("Images/Close.svg", null, WebInspector.UIString("Close resource view")));
closeButton.element.classList.add("close");
closeButton.addEventListener(WebInspector.TreeElementStatusButton.Event.Clicked, this._treeElementCloseButtonClicked, this);
fragment.appendChild(closeButton.element);
var goToButton = new WebInspector.TreeElementStatusButton(WebInspector.createGoToArrowButton());
goToButton.__treeElement = treeElement;
goToButton.addEventListener(WebInspector.TreeElementStatusButton.Event.Clicked, this._treeElementGoToArrowWasClicked, this);
fragment.appendChild(goToButton.element);
treeElement.status = fragment;
}
canShowDifferentContentView()
{
if (this._clickedTreeElementGoToArrow)
return true;
if (this.contentBrowser.currentContentView instanceof WebInspector.TimelineRecordingContentView)
return false;
return !this.restoringState || !this._restoredShowingTimelineRecordingContentView;
}
saveStateToCookie(cookie)
{
console.assert(cookie);
cookie[WebInspector.TimelineSidebarPanel.ShowingTimelineRecordingContentViewCookieKey] = this.contentBrowser.currentContentView instanceof WebInspector.TimelineRecordingContentView;
if (this._viewMode === WebInspector.TimelineSidebarPanel.ViewMode.RenderingFrames)
cookie[WebInspector.TimelineSidebarPanel.SelectedTimelineViewIdentifierCookieKey] = WebInspector.TimelineRecord.Type.RenderingFrame;
else {
var selectedTreeElement = this._timelinesTreeOutline.selectedTreeElement;
if (selectedTreeElement)
cookie[WebInspector.TimelineSidebarPanel.SelectedTimelineViewIdentifierCookieKey] = selectedTreeElement.representedObject.type;
else
cookie[WebInspector.TimelineSidebarPanel.SelectedTimelineViewIdentifierCookieKey] = WebInspector.TimelineSidebarPanel.OverviewTimelineIdentifierCookieValue;
}
super.saveStateToCookie(cookie);
}
restoreStateFromCookie(cookie, relaxedMatchDelay)
{
console.assert(cookie);
// The _displayedContentView is not ready on initial load, so delay the restore.
// This matches the delayed work in the WebInspector.TimelineSidebarPanel constructor.
if (!this._displayedContentView) {
setTimeout(this.restoreStateFromCookie.bind(this, cookie, relaxedMatchDelay), 0);
return;
}
this._restoredShowingTimelineRecordingContentView = cookie[WebInspector.TimelineSidebarPanel.ShowingTimelineRecordingContentViewCookieKey];
var selectedTimelineViewIdentifier = cookie[WebInspector.TimelineSidebarPanel.SelectedTimelineViewIdentifierCookieKey];
if (selectedTimelineViewIdentifier === WebInspector.TimelineRecord.Type.RenderingFrame && !WebInspector.FPSInstrument.supported())
selectedTimelineViewIdentifier = null;
if (selectedTimelineViewIdentifier && this._displayedRecording.timelines.has(selectedTimelineViewIdentifier))
this.showTimelineViewForTimeline(this._displayedRecording.timelines.get(selectedTimelineViewIdentifier));
else
this.showTimelineOverview();
// Don't call NavigationSidebarPanel.restoreStateFromCookie, because it tries to match based
// on type only as a last resort. This would cause the first recording to be reselected on reload.
}
// Private
_toggleRecordingOnSpacebar(event)
{
if (WebInspector.isEventTargetAnEditableField(event))
return;
this._toggleRecording();
event.preventDefault();
}
_toggleNewRecordingOnSpacebar(event)
{
if (WebInspector.isEventTargetAnEditableField(event))
return;
this._toggleRecording(true);
event.preventDefault();
}
_toggleRecording(shouldCreateRecording)
{
if (WebInspector.timelineManager.isCapturing()) {
WebInspector.timelineManager.stopCapturing();
this._recordGlyphElement.title = WebInspector.UIString("Click or press the spacebar to record.")
} else {
WebInspector.timelineManager.startCapturing(shouldCreateRecording);
// Show the timeline to which events will be appended.
this._recordingLoaded();
this._recordGlyphElement.title = WebInspector.UIString("Click or press the spacebar to stop recording.")
}
}
_treeElementGoToArrowWasClicked(event)
{
this._clickedTreeElementGoToArrow = true;
var treeElement = event.target.__treeElement;
console.assert(treeElement instanceof WebInspector.TreeElement);
treeElement.select(true, true);
this._clickedTreeElementGoToArrow = false;
}
_treeElementCloseButtonClicked(event)
{
var currentTimelineView = this._displayedContentView ? this._displayedContentView.currentTimelineView : null;
if (currentTimelineView && currentTimelineView.representedObject instanceof WebInspector.Timeline)
this.showTimelineViewForTimeline(currentTimelineView.representedObject);
else
this.showTimelineOverview();
}
_recordingsTreeSelectionDidChange(event)
{
let treeElement = event.data.selectedElement;
if (!treeElement)
return;
console.assert(treeElement.representedObject instanceof WebInspector.TimelineRecording);
this._recordingSelected(treeElement.representedObject);
}
_renderingFrameTimelineTimesUpdated(event)
{
if (this.viewMode === WebInspector.TimelineSidebarPanel.ViewMode.RenderingFrames)
this._refreshFrameSelectionChart();
}
_timelinesTreeSelectionDidChange(event)
{
let treeElement = event.data.selectedElement;
if (!treeElement)
return;
console.assert(this._timelineTreeElementMap.get(treeElement.representedObject) === treeElement, treeElement);
// If not selected by user, then this selection merely synced the tree element with the content view's contents.
let selectedByUser = event.data.selectedByUser;
if (!selectedByUser) {
console.assert(this._displayedContentView.currentTimelineView.representedObject === treeElement.representedObject);
return;
}
let timeline = treeElement.representedObject;
console.assert(timeline instanceof WebInspector.Timeline, timeline);
console.assert(this._displayedRecording.timelines.get(timeline.type) === timeline, timeline);
this._previousSelectedTimelineType = timeline.type;
this._displayedContentView.showTimelineViewForTimeline(timeline);
this.contentBrowser.showContentView(this._displayedContentView);
}
_contentBrowserCurrentContentViewDidChange(event)
{
var didShowTimelineRecordingContentView = this.contentBrowser.currentContentView instanceof WebInspector.TimelineRecordingContentView;
this.element.classList.toggle(WebInspector.TimelineSidebarPanel.TimelineRecordingContentViewShowingStyleClass, didShowTimelineRecordingContentView);
if (this.viewMode === WebInspector.TimelineSidebarPanel.ViewMode.RenderingFrames)
this._refreshFrameSelectionChart();
}
_capturingStarted(event)
{
this._recordStatusElement.textContent = WebInspector.UIString("Recording");
this._recordGlyphElement.classList.add(WebInspector.TimelineSidebarPanel.RecordGlyphRecordingStyleClass);
}
_capturingStopped(event)
{
this._recordStatusElement.textContent = "";
this._recordGlyphElement.classList.remove(WebInspector.TimelineSidebarPanel.RecordGlyphRecordingStyleClass);
}
_recordingCreated(event)
{
this._addRecording(event.data.recording)
this._recordingCountChanged();
}
_addRecording(recording)
{
console.assert(recording instanceof WebInspector.TimelineRecording, recording);
var recordingTreeElement = new WebInspector.GeneralTreeElement(WebInspector.TimelineSidebarPanel.StopwatchIconStyleClass, recording.displayName, null, recording);
this._recordingTreeElementMap.set(recording, recordingTreeElement);
this._recordingsTreeOutline.appendChild(recordingTreeElement);
}
_recordingCountChanged()
{
var previousTreeElement = null;
for (var treeElement of this._recordingTreeElementMap.values()) {
if (previousTreeElement) {
previousTreeElement.nextSibling = treeElement;
treeElement.previousSibling = previousTreeElement;
}
previousTreeElement = treeElement;
}
}
_recordingSelected(recording)
{
console.assert(recording instanceof WebInspector.TimelineRecording, recording);
var oldRecording = this._displayedRecording || null;
if (oldRecording) {
oldRecording.removeEventListener(WebInspector.TimelineRecording.Event.InstrumentAdded, this._instrumentAdded, this);
oldRecording.removeEventListener(WebInspector.TimelineRecording.Event.InstrumentRemoved, this._instrumentRemoved, this);
// Destroy tree elements in one operation to avoid unnecessary fixups.
this._timelinesTreeOutline.removeChildren();
this._timelineTreeElementMap.clear();
}
this._clearInstruments();
this._displayedRecording = recording;
this._displayedRecording.addEventListener(WebInspector.TimelineRecording.Event.InstrumentAdded, this._instrumentAdded, this);
this._displayedRecording.addEventListener(WebInspector.TimelineRecording.Event.InstrumentRemoved, this._instrumentRemoved, this);
for (let instrument of recording.instruments)
this._instrumentAdded(instrument);
// Save the current state incase we need to restore it to a new recording.
var cookie = {};
this.saveStateToCookie(cookie);
// Try to get the recording content view if it exists already, if it does we don't want to restore the cookie.
var onlyExisting = true;
this._displayedContentView = this.contentBrowser.contentViewForRepresentedObject(this._displayedRecording, onlyExisting, {timelineSidebarPanel: this});
if (this._displayedContentView) {
// Show the timeline that was being shown to update the sidebar tree state.
var currentTimelineView = this._displayedContentView.currentTimelineView;
if (currentTimelineView && currentTimelineView.representedObject instanceof WebInspector.Timeline)
this.showTimelineViewForTimeline(currentTimelineView.representedObject);
else
this.showTimelineOverview();
this.updateFilter();
return;
}
onlyExisting = false;
this._displayedContentView = this.contentBrowser.contentViewForRepresentedObject(this._displayedRecording, onlyExisting, {timelineSidebarPanel: this});
// Restore the cookie to carry over the previous recording view state to the new recording.
this.restoreStateFromCookie(cookie);
this.updateFilter();
}
_recordingLoaded(event)
{
this._recordingSelected(WebInspector.timelineManager.activeRecording);
}
_clearInstruments()
{
this._timelineTreeElementMap.clear();
if (WebInspector.FPSInstrument.supported())
this._removedFPSInstrument();
}
_instrumentAdded(instrumentOrEvent)
{
let instrument = instrumentOrEvent instanceof WebInspector.Instrument ? instrumentOrEvent : instrumentOrEvent.data.instrument;
console.assert(instrument instanceof WebInspector.Instrument, instrument);
let timeline = this._displayedRecording.timelineForInstrument(instrument);
console.assert(!this._timelineTreeElementMap.has(timeline), timeline);
if (timeline.type === WebInspector.TimelineRecord.Type.RenderingFrame) {
this._addedFPSInstrument();
timeline.addEventListener(WebInspector.Timeline.Event.TimesUpdated, this._renderingFrameTimelineTimesUpdated, this);
return;
}
let displayName = WebInspector.TimelineSidebarPanel.displayNameForTimeline(timeline);
let iconClassName = WebInspector.TimelineSidebarPanel.iconClassNameForTimeline(timeline);
let timelineTreeElement = new WebInspector.GeneralTreeElement([iconClassName, WebInspector.TimelineSidebarPanel.LargeIconStyleClass], displayName, null, timeline);
let tooltip = WebInspector.UIString("Close %s timeline view").format(displayName);
let button = new WebInspector.TreeElementStatusButton(useSVGSymbol("Images/CloseLarge.svg", "close-button", tooltip));
button.addEventListener(WebInspector.TreeElementStatusButton.Event.Clicked, this.showTimelineOverview, this);
timelineTreeElement.status = button.element;
this._timelinesTreeOutline.appendChild(timelineTreeElement);
this._timelineTreeElementMap.set(timeline, timelineTreeElement);
this._timelineCountChanged();
}
_instrumentRemoved(event)
{
let instrument = event.data.instrument;
console.assert(instrument instanceof WebInspector.Instrument, instrument);
let timeline = this._displayedRecording.timelineForInstrument(instrument);
if (timeline.type === WebInspector.TimelineRecord.Type.RenderingFrame) {
this._removedFPSInstrument();
timeline.removeEventListener(WebInspector.Timeline.Event.TimesUpdated, this._renderingFrameTimelineTimesUpdated, this);
return;
}
console.assert(this._timelineTreeElementMap.has(timeline), timeline);
let timelineTreeElement = this._timelineTreeElementMap.take(timeline);
let shouldSuppressOnDeselect = false;
let shouldSuppressSelectSibling = true;
this._timelinesTreeOutline.removeChild(timelineTreeElement, shouldSuppressOnDeselect, shouldSuppressSelectSibling);
this._timelineTreeElementMap.delete(timeline);
this._timelineCountChanged();
}
_addedFPSInstrument()
{
this._basicTitleBar.hidden = true;
this._renderingFramesTitleBar.hidden = false;
}
_removedFPSInstrument()
{
this._basicTitleBar.hidden = false;
this._renderingFramesTitleBar.hidden = true;
}
_timelineCountChanged()
{
var previousTreeElement = null;
for (var treeElement of this._timelineTreeElementMap.values()) {
if (previousTreeElement) {
previousTreeElement.nextSibling = treeElement;
treeElement.previousSibling = previousTreeElement;
}
previousTreeElement = treeElement;
}
this._updateTimelineOverviewHeight();
}
_updateTimelineOverviewHeight()
{
const timelineHeight = 36;
const eventTitleBarOffset = 58;
const contentElementOffset = 81;
const renderingFramesTimelineHeight = 108;
if (!this._displayedContentView)
return;
let overviewHeight;
let currentTimelineView = this._displayedContentView.currentTimelineView;
if (currentTimelineView && currentTimelineView.representedObject.type === WebInspector.TimelineRecord.Type.RenderingFrame)
overviewHeight = renderingFramesTimelineHeight;
else {
let timelineCount = this._timelineTreeElementMap.size;
if (this._timelineTreeElementMap.has(WebInspector.TimelineRecord.Type.RenderingFrame))
timelineCount--;
overviewHeight = timelineCount * timelineHeight;
}
this._timelineEventsTitleBarContainer.style.top = (overviewHeight + eventTitleBarOffset) + "px";
this.contentElement.style.top = (overviewHeight + contentElementOffset) + "px";
}
_recordGlyphMousedOver(event)
{
this._recordGlyphElement.classList.remove(WebInspector.TimelineSidebarPanel.RecordGlyphRecordingForcedStyleClass);
if (WebInspector.timelineManager.isCapturing())
this._recordStatusElement.textContent = WebInspector.UIString("Stop Recording");
else
this._recordStatusElement.textContent = WebInspector.UIString("Start Recording");
}
_recordGlyphMousedOut(event)
{
this._recordGlyphElement.classList.remove(WebInspector.TimelineSidebarPanel.RecordGlyphRecordingForcedStyleClass);
if (WebInspector.timelineManager.isCapturing())
this._recordStatusElement.textContent = WebInspector.UIString("Recording");
else
this._recordStatusElement.textContent = "";
}
_recordGlyphClicked(event)
{
// Add forced class to prevent the glyph from showing a confusing status after click.
this._recordGlyphElement.classList.add(WebInspector.TimelineSidebarPanel.RecordGlyphRecordingForcedStyleClass);
this._toggleRecording(event.shiftKey);
}
_viewModeSelected(event)
{
console.assert(event.target.selectedNavigationItem);
if (!event.target.selectedNavigationItem)
return;
var selectedNavigationItem = event.target.selectedNavigationItem;
var selectedByUser = true;
this._changeViewMode(selectedNavigationItem.identifier, selectedByUser);
}
_viewModeForTimeline(timeline)
{
if (timeline && timeline.type === WebInspector.TimelineRecord.Type.RenderingFrame)
return WebInspector.TimelineSidebarPanel.ViewMode.RenderingFrames;
return WebInspector.TimelineSidebarPanel.ViewMode.Timelines;
}
_changeViewMode(mode, selectedByUser)
{
if (!WebInspector.FPSInstrument.supported() || this._viewMode === mode)
return;
this._viewMode = mode;
this._viewModeNavigationBar.selectedNavigationItem = this._viewMode;
if (this._viewMode === WebInspector.TimelineSidebarPanel.ViewMode.Timelines) {
this._timelinesTreeOutline.element.classList.remove(WebInspector.NavigationSidebarPanel.ContentTreeOutlineElementHiddenStyleClassName);
this._frameSelectionChartSection.collapsed = true;
} else {
this._timelinesTreeOutline.element.classList.add(WebInspector.NavigationSidebarPanel.ContentTreeOutlineElementHiddenStyleClassName);
this._frameSelectionChartSection.collapsed = false;
}
if (selectedByUser) {
var timelineType = this._previousSelectedTimelineType;
if (this._viewMode === WebInspector.TimelineSidebarPanel.ViewMode.RenderingFrames) {
this._previousSelectedTimelineType = this._timelinesTreeOutline.selectedTreeElement ? this._timelinesTreeOutline.selectedTreeElement.representedObject.type : null;
timelineType = WebInspector.TimelineRecord.Type.RenderingFrame;
}
if (timelineType) {
console.assert(this._displayedRecording.timelines.has(timelineType), timelineType);
this.showTimelineViewForTimeline(this._displayedRecording.timelines.get(timelineType));
} else
this.showTimelineOverview();
}
this._timelineCountChanged();
this.updateFilter();
}
_frameSelectionLegendItemChecked(event)
{
if (event.data.checked)
this._renderingFrameTaskFilter.delete(event.data.id);
else
this._renderingFrameTaskFilter.add(event.data.id);
this.updateFilter();
}
_refreshFrameSelectionChart()
{
if (!this.visible)
return;
function getSelectedRecords()
{
console.assert(this._displayedRecording);
console.assert(this._displayedRecording.timelines.has(WebInspector.TimelineRecord.Type.RenderingFrame), "Cannot find rendering frames timeline in displayed recording");
var timeline = this._displayedRecording.timelines.get(WebInspector.TimelineRecord.Type.RenderingFrame);
var selectedRecords = [];
for (var record of timeline.records) {
console.assert(record instanceof WebInspector.RenderingFrameTimelineRecord);
// If this frame is completely before the bounds of the graph, skip this record.
if (record.frameIndex < this._startFrameIndex)
continue;
// If this record is completely after the end time, break out now.
// Records are sorted, so all records after this will be beyond the end time too.
if (record.frameIndex > this._endFrameIndex)
break;
selectedRecords.push(record);
}
return selectedRecords;
}
let records = getSelectedRecords.call(this);
for (let key in WebInspector.RenderingFrameTimelineRecord.TaskType) {
let taskType = WebInspector.RenderingFrameTimelineRecord.TaskType[key];
let value = records.reduce(function(previousValue, currentValue) { return previousValue + currentValue.durationForTask(taskType); }, 0);
this._frameSelectionChartRow.setItemValue(taskType, value);
}
if (!records.length) {
this._frameSelectionChartRow.title = WebInspector.UIString("Frames: None Selected");
return;
}
var firstRecord = records[0];
var lastRecord = records.lastValue;
if (records.length > 1) {
this._frameSelectionChartRow.title = WebInspector.UIString("Frames: %d \u2013 %d (%s \u2013 %s)").format(firstRecord.frameNumber, lastRecord.frameNumber,
Number.secondsToString(firstRecord.startTime), Number.secondsToString(lastRecord.endTime));
} else {
this._frameSelectionChartRow.title = WebInspector.UIString("Frame: %d (%s \u2013 %s)").format(firstRecord.frameNumber,
Number.secondsToString(firstRecord.startTime), Number.secondsToString(lastRecord.endTime));
}
}
// These methods are only used when ReplayAgent is available.
_updateReplayInterfaceVisibility()
{
var shouldShowReplayInterface = !!(window.ReplayAgent && WebInspector.showReplayInterfaceSetting.value);
this._statusBarElement.classList.toggle(WebInspector.TimelineSidebarPanel.HiddenStyleClassName, shouldShowReplayInterface);
this._replayNavigationBar.element.classList.toggle(WebInspector.TimelineSidebarPanel.HiddenStyleClassName, !shouldShowReplayInterface);
}
_contextMenuNavigationBarOrStatusBar()
{
if (!window.ReplayAgent)
return;
let toggleReplayInterface = () => {
WebInspector.showReplayInterfaceSetting.value = !WebInspector.showReplayInterfaceSetting.value;
};
let contextMenu = WebInspector.ContextMenu.createFromEvent(event);
if (WebInspector.showReplayInterfaceSetting.value)
contextMenu.appendItem(WebInspector.UIString("Hide Replay Controls"), toggleReplayInterface);
else
contextMenu.appendItem(WebInspector.UIString("Show Replay Controls"), toggleReplayInterface);
}
_replayCaptureButtonClicked()
{
if (!this._replayCaptureButtonItem.activated) {
WebInspector.replayManager.startCapturing();
WebInspector.timelineManager.startCapturing();
// De-bounce further presses until the backend has begun capturing.
this._replayCaptureButtonItem.activated = true;
this._replayCaptureButtonItem.enabled = false;
} else {
WebInspector.replayManager.stopCapturing();
WebInspector.timelineManager.stopCapturing();
this._replayCaptureButtonItem.enabled = false;
}
}
_replayPauseResumeButtonClicked()
{
if (this._replayPauseResumeButtonItem.toggled)
WebInspector.replayManager.pausePlayback();
else
WebInspector.replayManager.replayToCompletion();
}
_captureStarted()
{
this._replayCaptureButtonItem.enabled = true;
}
_captureStopped()
{
this._replayCaptureButtonItem.activated = false;
this._replayPauseResumeButtonItem.enabled = true;
}
_playbackStarted()
{
this._replayPauseResumeButtonItem.toggled = true;
}
_playbackPaused()
{
this._replayPauseResumeButtonItem.toggled = false;
}
};
WebInspector.TimelineSidebarPanel.ViewMode = {
Timelines: "timeline-sidebar-panel-view-mode-timelines",
RenderingFrames: "timeline-sidebar-panel-view-mode-frames"
};
WebInspector.TimelineSidebarPanel.HiddenStyleClassName = "hidden";
WebInspector.TimelineSidebarPanel.StatusBarStyleClass = "status-bar";
WebInspector.TimelineSidebarPanel.RecordGlyphStyleClass = "record-glyph";
WebInspector.TimelineSidebarPanel.RecordGlyphRecordingStyleClass = "recording";
WebInspector.TimelineSidebarPanel.RecordGlyphRecordingForcedStyleClass = "forced";
WebInspector.TimelineSidebarPanel.RecordStatusStyleClass = "record-status";
WebInspector.TimelineSidebarPanel.TitleBarStyleClass = "title-bar";
WebInspector.TimelineSidebarPanel.TitleBarTextStyleClass = "title-bar-text";
WebInspector.TimelineSidebarPanel.TitleBarScopeBarStyleClass = "title-bar-scope-bar";
WebInspector.TimelineSidebarPanel.TimelinesTitleBarStyleClass = "timelines";
WebInspector.TimelineSidebarPanel.TimelineEventsTitleBarStyleClass = "timeline-events";
WebInspector.TimelineSidebarPanel.TimelinesContentContainerStyleClass = "timelines-content";
WebInspector.TimelineSidebarPanel.LargeIconStyleClass = "large";
WebInspector.TimelineSidebarPanel.StopwatchIconStyleClass = "stopwatch-icon";
WebInspector.TimelineSidebarPanel.TimelineRecordingContentViewShowingStyleClass = "timeline-recording-content-view-showing";
WebInspector.TimelineSidebarPanel.ShowingTimelineRecordingContentViewCookieKey = "timeline-sidebar-panel-showing-timeline-recording-content-view";
WebInspector.TimelineSidebarPanel.SelectedTimelineViewIdentifierCookieKey = "timeline-sidebar-panel-selected-timeline-view-identifier";
WebInspector.TimelineSidebarPanel.OverviewTimelineIdentifierCookieValue = "overview";