TimelinePanel.js   [plain text]


/*
 * Copyright (C) 2009 Google 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:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * 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.
 *     * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 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 THE COPYRIGHT
 * OWNER OR 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.TimelinePanel = function()
{
    WebInspector.Panel.call(this);
    this.element.addStyleClass("timeline");

    this._overviewPane = new WebInspector.TimelineOverviewPane(this.categories);
    this._overviewPane.addEventListener("window changed", this._windowChanged, this);
    this._overviewPane.addEventListener("filter changed", this._refresh, this);
    this.element.appendChild(this._overviewPane.element);
    this.element.tabIndex = 0;

    this._sidebarBackgroundElement = document.createElement("div");
    this._sidebarBackgroundElement.className = "sidebar timeline-sidebar-background";
    this.element.appendChild(this._sidebarBackgroundElement);

    this._containerElement = document.createElement("div");
    this._containerElement.id = "timeline-container";
    this._containerElement.addEventListener("scroll", this._onScroll.bind(this), false);
    this.element.appendChild(this._containerElement);

    this.createSidebar(this._containerElement, this._containerElement);
    var itemsTreeElement = new WebInspector.SidebarSectionTreeElement(WebInspector.UIString("RECORDS"), {}, true);
    itemsTreeElement.expanded = true;
    this.sidebarTree.appendChild(itemsTreeElement);

    this._sidebarListElement = document.createElement("div");
    this.sidebarElement.appendChild(this._sidebarListElement);

    this._containerContentElement = document.createElement("div");
    this._containerContentElement.id = "resources-container-content";
    this._containerElement.appendChild(this._containerContentElement);

    this._timelineGrid = new WebInspector.TimelineGrid();
    this._itemsGraphsElement = this._timelineGrid.itemsGraphsElement;
    this._itemsGraphsElement.id = "timeline-graphs";
    this._containerContentElement.appendChild(this._timelineGrid.element);

    this._topGapElement = document.createElement("div");
    this._topGapElement.className = "timeline-gap";
    this._itemsGraphsElement.appendChild(this._topGapElement);

    this._graphRowsElement = document.createElement("div");
    this._itemsGraphsElement.appendChild(this._graphRowsElement);

    this._bottomGapElement = document.createElement("div");
    this._bottomGapElement.className = "timeline-gap";
    this._itemsGraphsElement.appendChild(this._bottomGapElement);

    this._rootRecord = this._createRootRecord();
    this._sendRequestRecords = {};
    this._timerRecords = {};

    this._calculator = new WebInspector.TimelineCalculator();
    this._calculator._showShortEvents = false;
    var shortRecordThresholdTitle = Number.secondsToString(WebInspector.TimelinePanel.shortRecordThreshold, WebInspector.UIString.bind(WebInspector));
    this._showShortRecordsTitleText = WebInspector.UIString("Show the records that are shorter than %s", shortRecordThresholdTitle);
    this._hideShortRecordsTitleText = WebInspector.UIString("Hide the records that are shorter than %s", shortRecordThresholdTitle);
    this._createStatusbarButtons();

    this._boundariesAreValid = true;
    this._scrollTop = 0;

    this._popoverHelper = new WebInspector.PopoverHelper(this._containerElement, this._getPopoverAnchor.bind(this), this._showPopover.bind(this), true);

    // Disable short events filter by default.
    this.toggleFilterButton.toggled = true;
    this._calculator._showShortEvents = this.toggleFilterButton.toggled;
    this._markTimelineRecords = [];
    this._expandOffset = 15;
}

WebInspector.TimelinePanel.shortRecordThreshold = 0.015;

WebInspector.TimelinePanel.prototype = {
    toolbarItemClass: "timeline",

    get toolbarItemLabel()
    {
        return WebInspector.UIString("Timeline");
    },

    get statusBarItems()
    {
        return [this.toggleFilterButton.element, this.toggleTimelineButton.element, this.clearButton.element, this.recordsCounter];
    },

    get categories()
    {
        if (!this._categories) {
            this._categories = {
                loading: new WebInspector.TimelineCategory("loading", WebInspector.UIString("Loading"), "rgb(47,102,236)"),
                scripting: new WebInspector.TimelineCategory("scripting", WebInspector.UIString("Scripting"), "rgb(157,231,119)"),
                rendering: new WebInspector.TimelineCategory("rendering", WebInspector.UIString("Rendering"), "rgb(164,60,255)")
            };
        }
        return this._categories;
    },

    get defaultFocusedElement()
    {
        return this.element;
    },

    get _recordStyles()
    {
        if (!this._recordStylesArray) {
            var recordTypes = WebInspector.TimelineAgent.RecordType;
            var recordStyles = {};
            recordStyles[recordTypes.EventDispatch] = { title: WebInspector.UIString("Event"), category: this.categories.scripting };
            recordStyles[recordTypes.Layout] = { title: WebInspector.UIString("Layout"), category: this.categories.rendering };
            recordStyles[recordTypes.RecalculateStyles] = { title: WebInspector.UIString("Recalculate Style"), category: this.categories.rendering };
            recordStyles[recordTypes.Paint] = { title: WebInspector.UIString("Paint"), category: this.categories.rendering };
            recordStyles[recordTypes.ParseHTML] = { title: WebInspector.UIString("Parse"), category: this.categories.loading };
            recordStyles[recordTypes.TimerInstall] = { title: WebInspector.UIString("Install Timer"), category: this.categories.scripting };
            recordStyles[recordTypes.TimerRemove] = { title: WebInspector.UIString("Remove Timer"), category: this.categories.scripting };
            recordStyles[recordTypes.TimerFire] = { title: WebInspector.UIString("Timer Fired"), category: this.categories.scripting };
            recordStyles[recordTypes.XHRReadyStateChange] = { title: WebInspector.UIString("XHR Ready State Change"), category: this.categories.scripting };
            recordStyles[recordTypes.XHRLoad] = { title: WebInspector.UIString("XHR Load"), category: this.categories.scripting };
            recordStyles[recordTypes.EvaluateScript] = { title: WebInspector.UIString("Evaluate Script"), category: this.categories.scripting };
            recordStyles[recordTypes.MarkTimeline] = { title: WebInspector.UIString("Mark"), category: this.categories.scripting };
            recordStyles[recordTypes.ResourceSendRequest] = { title: WebInspector.UIString("Send Request"), category: this.categories.loading };
            recordStyles[recordTypes.ResourceReceiveResponse] = { title: WebInspector.UIString("Receive Response"), category: this.categories.loading };
            recordStyles[recordTypes.ResourceFinish] = { title: WebInspector.UIString("Finish Loading"), category: this.categories.loading };
            recordStyles[recordTypes.FunctionCall] = { title: WebInspector.UIString("Function Call"), category: this.categories.scripting };
            recordStyles[recordTypes.ResourceReceiveData] = { title: WebInspector.UIString("Receive Data"), category: this.categories.loading };
            recordStyles[recordTypes.GCEvent] = { title: WebInspector.UIString("GC Event"), category: this.categories.scripting };
            recordStyles[recordTypes.MarkDOMContentEventType] = { title: WebInspector.UIString("DOMContent event"), category: this.categories.scripting };
            recordStyles[recordTypes.MarkLoadEventType] = { title: WebInspector.UIString("Load event"), category: this.categories.scripting };
            this._recordStylesArray = recordStyles;
        }
        return this._recordStylesArray;
    },

    _createStatusbarButtons: function()
    {
        this.toggleTimelineButton = new WebInspector.StatusBarButton(WebInspector.UIString("Record"), "record-profile-status-bar-item");
        this.toggleTimelineButton.addEventListener("click", this._toggleTimelineButtonClicked.bind(this), false);

        this.clearButton = new WebInspector.StatusBarButton(WebInspector.UIString("Clear"), "timeline-clear-status-bar-item");
        this.clearButton.addEventListener("click", this._clearPanel.bind(this), false);

        this.toggleFilterButton = new WebInspector.StatusBarButton(this._hideShortRecordsTitleText, "timeline-filter-status-bar-item");
        this.toggleFilterButton.addEventListener("click", this._toggleFilterButtonClicked.bind(this), false);

        this.recordsCounter = document.createElement("span");
        this.recordsCounter.className = "timeline-records-counter";
    },

    _updateRecordsCounter: function()
    {
        this.recordsCounter.textContent = WebInspector.UIString("%d of %d captured records are visible", this._rootRecord._visibleRecordsCount, this._rootRecord._allRecordsCount);
    },

    _updateEventDividers: function()
    {
        this._timelineGrid.removeEventDividers();
        var clientWidth = this._graphRowsElement.offsetWidth - this._expandOffset;
        var dividers = [];
        for (var i = 0; i < this._markTimelineRecords.length; ++i) {
            var record = this._markTimelineRecords[i];
            var positions = this._calculator.computeBarGraphWindowPosition(record, clientWidth);
            var dividerPosition = Math.round(positions.left);
            if (dividerPosition < 0 || dividerPosition >= clientWidth || dividers[dividerPosition])
                continue;
            var divider = this._createEventDivider(record);
            divider.style.left = (dividerPosition + this._expandOffset) + "px";
            dividers[dividerPosition] = divider;
        }
        this._timelineGrid.addEventDividers(dividers);
        this._overviewPane.updateEventDividers(this._markTimelineRecords, this._createEventDivider.bind(this));
    },

    _createEventDivider: function(record)
    {
        var eventDivider = document.createElement("div");
        eventDivider.className = "resources-event-divider";
        var recordTypes = WebInspector.TimelineAgent.RecordType;

        var eventDividerPadding = document.createElement("div");
        eventDividerPadding.className = "resources-event-divider-padding";
        eventDividerPadding.title = record.title;

        if (record.type === recordTypes.MarkDOMContentEventType)
            eventDivider.className += " resources-blue-divider";
        else if (record.type === recordTypes.MarkLoadEventType)
            eventDivider.className += " resources-red-divider";
        else if (record.type === recordTypes.MarkTimeline) {
            eventDivider.className += " resources-orange-divider";
            eventDividerPadding.title = record.data.message;
        }
        eventDividerPadding.appendChild(eventDivider);
        return eventDividerPadding;
    },

    _toggleTimelineButtonClicked: function()
    {
        if (this.toggleTimelineButton.toggled)
            InspectorBackend.stopTimelineProfiler();
        else {
            this._clearPanel();
            InspectorBackend.startTimelineProfiler();
        }
    },

    _toggleFilterButtonClicked: function()
    {
        this.toggleFilterButton.toggled = !this.toggleFilterButton.toggled;
        this._calculator._showShortEvents = this.toggleFilterButton.toggled;
        this.toggleFilterButton.element.title = this._calculator._showShortEvents ? this._hideShortRecordsTitleText : this._showShortRecordsTitleText;
        this._scheduleRefresh(true);
    },

    timelineWasStarted: function()
    {
        this.toggleTimelineButton.toggled = true;
    },

    timelineWasStopped: function()
    {
        this.toggleTimelineButton.toggled = false;
    },

    addRecordToTimeline: function(record)
    {
        if (record.type == WebInspector.TimelineAgent.RecordType.ResourceSendRequest && record.data.isMainResource) {
            if (this._mainResourceIdentifier != record.data.identifier) {
                // We are loading new main resource -> clear the panel. Check above is necessary since
                // there may be several resource loads with main resource marker upon redirects, redirects are reported with
                // the original identifier.
                this._mainResourceIdentifier = record.data.identifier;
                this._clearPanel();
            }
        }
        this._innerAddRecordToTimeline(record, this._rootRecord);
        this._scheduleRefresh();
    },

    _findParentRecord: function(record)
    {
        var recordTypes = WebInspector.TimelineAgent.RecordType;
        var parentRecord;
        if (record.type === recordTypes.ResourceReceiveResponse ||
            record.type === recordTypes.ResourceFinish ||
            record.type === recordTypes.ResourceReceiveData)
            parentRecord = this._sendRequestRecords[record.data.identifier];
        else if (record.type === recordTypes.TimerFire)
            parentRecord = this._timerRecords[record.data.timerId];
        return parentRecord;
    },

    _innerAddRecordToTimeline: function(record, parentRecord)
    {
        var connectedToOldRecord = false;
        var recordTypes = WebInspector.TimelineAgent.RecordType;
        if (record.type === recordTypes.MarkDOMContentEventType || record.type === recordTypes.MarkLoadEventType)
            parentRecord = null; // No bar entry for load events.
        else if (parentRecord === this._rootRecord) {
            var newParentRecord = this._findParentRecord(record);
            if (newParentRecord) {
                parentRecord = newParentRecord;
                connectedToOldRecord = true;
            }
        }

        if (record.type == recordTypes.TimerFire && record.children && record.children.length === 1) {
            var childRecord = record.children[0];
            if ( childRecord.type === recordTypes.FunctionCall) {
                record.data.scriptName = childRecord.data.scriptName;
                record.data.scriptLine = childRecord.data.scriptLine;
                record.children = childRecord.children;
            }
        }

        var formattedRecord = new WebInspector.TimelinePanel.FormattedRecord(record, parentRecord, this._recordStyles, this._sendRequestRecords, this._timerRecords);

        if (record.type === recordTypes.MarkDOMContentEventType || record.type === recordTypes.MarkLoadEventType) {
            this._markTimelineRecords.push(formattedRecord);
            return;
        }

        ++this._rootRecord._allRecordsCount;
        formattedRecord.collapsed = (parentRecord === this._rootRecord);

        var childrenCount = record.children ? record.children.length : 0;
        for (var i = 0; i < childrenCount; ++i)
            this._innerAddRecordToTimeline(record.children[i], formattedRecord);

        formattedRecord._calculateAggregatedStats(this.categories);

        if (connectedToOldRecord) {
            var record = formattedRecord;
            do {
                var parent = record.parent;
                parent._cpuTime += formattedRecord._cpuTime;
                if (parent._lastChildEndTime < record._lastChildEndTime)
                    parent._lastChildEndTime = record._lastChildEndTime;
                for (var category in formattedRecord._aggregatedStats)
                    parent._aggregatedStats[category] += formattedRecord._aggregatedStats[category];
                record = parent;
            } while (record.parent);
        } else
            if (parentRecord !== this._rootRecord)
                parentRecord._selfTime -= formattedRecord.endTime - formattedRecord.startTime;

        // Keep bar entry for mark timeline since nesting might be interesting to the user.
        if (record.type === recordTypes.MarkTimeline)
            this._markTimelineRecords.push(formattedRecord);
    },

    setSidebarWidth: function(width)
    {
        WebInspector.Panel.prototype.setSidebarWidth.call(this, width);
        this._sidebarBackgroundElement.style.width = width + "px";
        this._overviewPane.setSidebarWidth(width);
    },

    updateMainViewWidth: function(width)
    {
        this._containerContentElement.style.left = width + "px";
        this._scheduleRefresh();
        this._overviewPane.updateMainViewWidth(width);
    },

    resize: function()
    {
        this._closeRecordDetails();
        this._scheduleRefresh();
    },

    _createRootRecord: function()
    {
        var rootRecord = {};
        rootRecord.children = [];
        rootRecord._visibleRecordsCount = 0;
        rootRecord._allRecordsCount = 0;
        rootRecord._aggregatedStats = {};
        return rootRecord;
    },

    _clearPanel: function()
    {
        this._markTimelineRecords = [];
        this._sendRequestRecords = {};
        this._timerRecords = {};
        this._rootRecord = this._createRootRecord();
        this._boundariesAreValid = false;
        this._overviewPane.reset();
        this._adjustScrollPosition(0);
        this._refresh();
        this._closeRecordDetails();
    },

    show: function()
    {
        WebInspector.Panel.prototype.show.call(this);
        if (typeof this._scrollTop === "number")
            this._containerElement.scrollTop = this._scrollTop;
        this._refresh();
    },

    hide: function()
    {
        WebInspector.Panel.prototype.hide.call(this);
        this._closeRecordDetails();
    },

    _onScroll: function(event)
    {
        this._closeRecordDetails();
        var scrollTop = this._containerElement.scrollTop;
        var dividersTop = Math.max(0, scrollTop);
        this._timelineGrid.setScrollAndDividerTop(scrollTop, dividersTop);
        this._scheduleRefresh(true);
    },

    _windowChanged: function()
    {
        this._closeRecordDetails();
        this._scheduleRefresh();
    },

    _scheduleRefresh: function(preserveBoundaries)
    {
        this._closeRecordDetails();
        this._boundariesAreValid &= preserveBoundaries;

        if (!this.visible)
            return;

        if (preserveBoundaries)
            this._refresh();
        else
            if (!this._refreshTimeout)
                this._refreshTimeout = setTimeout(this._refresh.bind(this), 100);
    },

    _refresh: function()
    {
        if (this._refreshTimeout) {
            clearTimeout(this._refreshTimeout);
            delete this._refreshTimeout;
        }

        this._overviewPane.update(this._rootRecord.children, this._calculator._showShortEvents);
        this._refreshRecords(!this._boundariesAreValid);
        this._updateRecordsCounter();
        if(!this._boundariesAreValid)
            this._updateEventDividers();
        this._boundariesAreValid = true;
    },

    _updateBoundaries: function()
    {
        this._calculator.reset();
        this._calculator.windowLeft = this._overviewPane.windowLeft;
        this._calculator.windowRight = this._overviewPane.windowRight;

        for (var i = 0; i < this._rootRecord.children.length; ++i)
            this._calculator.updateBoundaries(this._rootRecord.children[i]);

        this._calculator.calculateWindow();
    },

    _addToRecordsWindow: function(record, recordsWindow, parentIsCollapsed)
    {
        if (!this._calculator._showShortEvents && !record.isLong())
            return;
        var percentages = this._calculator.computeBarGraphPercentages(record);
        if (percentages.start < 100 && percentages.endWithChildren >= 0 && !record.category.hidden) {
            ++this._rootRecord._visibleRecordsCount;
            ++record.parent._invisibleChildrenCount;
            if (!parentIsCollapsed)
                recordsWindow.push(record);
        }

        var index = recordsWindow.length;
        record._invisibleChildrenCount = 0;
        for (var i = 0; i < record.children.length; ++i)
            this._addToRecordsWindow(record.children[i], recordsWindow, parentIsCollapsed || record.collapsed);
        record._visibleChildrenCount = recordsWindow.length - index;
    },

    _filterRecords: function()
    {
        var recordsInWindow = [];
        this._rootRecord._visibleRecordsCount = 0;
        for (var i = 0; i < this._rootRecord.children.length; ++i)
            this._addToRecordsWindow(this._rootRecord.children[i], recordsInWindow);
        return recordsInWindow;
    },

    _refreshRecords: function(updateBoundaries)
    {
        if (updateBoundaries)
            this._updateBoundaries();

        var recordsInWindow = this._filterRecords();

        // Calculate the visible area.
        this._scrollTop = this._containerElement.scrollTop;
        var visibleTop = this._scrollTop;
        var visibleBottom = visibleTop + this._containerElement.clientHeight;

        // Define row height, should be in sync with styles for timeline graphs.
        const rowHeight = 18;

        // Convert visible area to visible indexes. Always include top-level record for a visible nested record.
        var startIndex = Math.max(0, Math.min(Math.floor(visibleTop / rowHeight) - 1, recordsInWindow.length - 1));
        var endIndex = Math.min(recordsInWindow.length, Math.ceil(visibleBottom / rowHeight));

        // Resize gaps first.
        const top = (startIndex * rowHeight) + "px";
        this._topGapElement.style.height = top;
        this.sidebarElement.style.top = top;
        this.sidebarResizeElement.style.top = top;
        this._bottomGapElement.style.height = (recordsInWindow.length - endIndex) * rowHeight + "px";

        // Update visible rows.
        var listRowElement = this._sidebarListElement.firstChild;
        var width = this._graphRowsElement.offsetWidth;
        this._itemsGraphsElement.removeChild(this._graphRowsElement);
        var graphRowElement = this._graphRowsElement.firstChild;
        var scheduleRefreshCallback = this._scheduleRefresh.bind(this, true);

        for (var i = startIndex; i < endIndex; ++i) {
            var record = recordsInWindow[i];
            var isEven = !(i % 2);

            if (!listRowElement) {
                listRowElement = new WebInspector.TimelineRecordListRow().element;
                this._sidebarListElement.appendChild(listRowElement);
            }
            if (!graphRowElement) {
                graphRowElement = new WebInspector.TimelineRecordGraphRow(this._itemsGraphsElement, scheduleRefreshCallback, rowHeight).element;
                this._graphRowsElement.appendChild(graphRowElement);
            }

            listRowElement.row.update(record, isEven, this._calculator, visibleTop);
            graphRowElement.row.update(record, isEven, this._calculator, width, this._expandOffset, i);

            listRowElement = listRowElement.nextSibling;
            graphRowElement = graphRowElement.nextSibling;
        }

        // Remove extra rows.
        while (listRowElement) {
            var nextElement = listRowElement.nextSibling;
            listRowElement.row.dispose();
            listRowElement = nextElement;
        }
        while (graphRowElement) {
            var nextElement = graphRowElement.nextSibling;
            graphRowElement.row.dispose();
            graphRowElement = nextElement;
        }

        this._itemsGraphsElement.insertBefore(this._graphRowsElement, this._bottomGapElement);
        this.sidebarResizeElement.style.height = this.sidebarElement.clientHeight + "px";
        // Reserve some room for expand / collapse controls to the left for records that start at 0ms.
        var timelinePaddingLeft = this._calculator.windowLeft === 0 ? this._expandOffset : 0;
        if (updateBoundaries)
            this._timelineGrid.updateDividers(true, this._calculator, timelinePaddingLeft);
        this._adjustScrollPosition((recordsInWindow.length + 1) * rowHeight);
    },

    _adjustScrollPosition: function(totalHeight)
    {
        // Prevent the container from being scrolled off the end.
        if ((this._containerElement.scrollTop + this._containerElement.offsetHeight) > totalHeight + 1)
            this._containerElement.scrollTop = (totalHeight - this._containerElement.offsetHeight);
    },

    _getPopoverAnchor: function(element)
    {
        return element.enclosingNodeOrSelfWithClass("timeline-graph-bar") || element.enclosingNodeOrSelfWithClass("timeline-tree-item");
    },

    _showPopover: function(anchor)
    {
        var record = anchor.row._record;
        var popover = new WebInspector.Popover(record._generatePopupContent(this._calculator, this.categories));
        popover.show(anchor);
        return popover;
    },

    _closeRecordDetails: function()
    {
        this._popoverHelper.hidePopup();
    }
}

WebInspector.TimelinePanel.prototype.__proto__ = WebInspector.Panel.prototype;

WebInspector.TimelineCategory = function(name, title, color)
{
    this.name = name;
    this.title = title;
    this.color = color;
}

WebInspector.TimelineCalculator = function()
{
    this.reset();
    this.windowLeft = 0.0;
    this.windowRight = 1.0;
    this._uiString = WebInspector.UIString.bind(WebInspector);
}

WebInspector.TimelineCalculator.prototype = {
    computeBarGraphPercentages: function(record)
    {
        var start = (record.startTime - this.minimumBoundary) / this.boundarySpan * 100;
        var end = (record.startTime + record._selfTime - this.minimumBoundary) / this.boundarySpan * 100;
        var endWithChildren = (record._lastChildEndTime - this.minimumBoundary) / this.boundarySpan * 100;
        var cpuWidth = record._cpuTime / this.boundarySpan * 100;
        return {start: start, end: end, endWithChildren: endWithChildren, cpuWidth: cpuWidth};
    },

    computeBarGraphWindowPosition: function(record, clientWidth)
    {
        const minWidth = 5;
        const borderWidth = 4;
        var workingArea = clientWidth - minWidth - borderWidth;
        var percentages = this.computeBarGraphPercentages(record);
        var left = percentages.start / 100 * workingArea;
        var width = (percentages.end - percentages.start) / 100 * workingArea + minWidth;
        var widthWithChildren =  (percentages.endWithChildren - percentages.start) / 100 * workingArea;
        var cpuWidth = percentages.cpuWidth / 100 * workingArea + minWidth;
        if (percentages.endWithChildren > percentages.end)
            widthWithChildren += borderWidth + minWidth;
        return {left: left, width: width, widthWithChildren: widthWithChildren, cpuWidth: cpuWidth};
    },

    calculateWindow: function()
    {
        this.minimumBoundary = this._absoluteMinimumBoundary + this.windowLeft * (this._absoluteMaximumBoundary - this._absoluteMinimumBoundary);
        this.maximumBoundary = this._absoluteMinimumBoundary + this.windowRight * (this._absoluteMaximumBoundary - this._absoluteMinimumBoundary);
        this.boundarySpan = this.maximumBoundary - this.minimumBoundary;
    },

    reset: function()
    {
        this._absoluteMinimumBoundary = -1;
        this._absoluteMaximumBoundary = -1;
    },

    updateBoundaries: function(record)
    {
        var lowerBound = record.startTime;
        if (this._absoluteMinimumBoundary === -1 || lowerBound < this._absoluteMinimumBoundary)
            this._absoluteMinimumBoundary = lowerBound;

        const minimumTimeFrame = 0.1;
        const minimumDeltaForZeroSizeEvents = 0.01;
        var upperBound = Math.max(record._lastChildEndTime + minimumDeltaForZeroSizeEvents, lowerBound + minimumTimeFrame);
        if (this._absoluteMaximumBoundary === -1 || upperBound > this._absoluteMaximumBoundary)
            this._absoluteMaximumBoundary = upperBound;
    },

    formatValue: function(value)
    {
        return Number.secondsToString(value + this.minimumBoundary - this._absoluteMinimumBoundary, this._uiString);
    }
}


WebInspector.TimelineRecordListRow = function()
{
    this.element = document.createElement("div");
    this.element.row = this;
    this.element.style.cursor = "pointer";
    var iconElement = document.createElement("span");
    iconElement.className = "timeline-tree-icon";
    this.element.appendChild(iconElement);

    this._typeElement = document.createElement("span");
    this._typeElement.className = "type";
    this.element.appendChild(this._typeElement);

    var separatorElement = document.createElement("span");
    separatorElement.className = "separator";
    separatorElement.textContent = " ";

    this._dataElement = document.createElement("span");
    this._dataElement.className = "data dimmed";

    this.element.appendChild(separatorElement);
    this.element.appendChild(this._dataElement);
}

WebInspector.TimelineRecordListRow.prototype = {
    update: function(record, isEven, calculator, offset)
    {
        this._record = record;
        this._calculator = calculator;
        this._offset = offset;

        this.element.className = "timeline-tree-item timeline-category-" + record.category.name + (isEven ? " even" : "");
        this._typeElement.textContent = record.title;

        if (this._dataElement.firstChild)
            this._dataElement.removeChildren();
        if (record.details) {
            var detailsContainer = document.createElement("span");
            if (typeof record.details === "object") {
                detailsContainer.appendChild(document.createTextNode("("));
                detailsContainer.appendChild(record.details);
                detailsContainer.appendChild(document.createTextNode(")"));
            } else
                detailsContainer.textContent = "(" + record.details + ")";
            this._dataElement.appendChild(detailsContainer);
        }
    },

    dispose: function()
    {
        this.element.parentElement.removeChild(this.element);
    }
}

WebInspector.TimelineRecordGraphRow = function(graphContainer, scheduleRefresh, rowHeight)
{
    this.element = document.createElement("div");
    this.element.row = this;

    this._barAreaElement = document.createElement("div");
    this._barAreaElement.className = "timeline-graph-bar-area";
    this.element.appendChild(this._barAreaElement);

    this._barWithChildrenElement = document.createElement("div");
    this._barWithChildrenElement.className = "timeline-graph-bar with-children";
    this._barWithChildrenElement.row = this;
    this._barAreaElement.appendChild(this._barWithChildrenElement);

    this._barCpuElement = document.createElement("div");
    this._barCpuElement.className = "timeline-graph-bar cpu"
    this._barCpuElement.row = this;
    this._barAreaElement.appendChild(this._barCpuElement);

    this._barElement = document.createElement("div");
    this._barElement.className = "timeline-graph-bar";
    this._barElement.row = this;
    this._barAreaElement.appendChild(this._barElement);

    this._expandElement = document.createElement("div");
    this._expandElement.className = "timeline-expandable";
    graphContainer.appendChild(this._expandElement);

    var leftBorder = document.createElement("div");
    leftBorder.className = "timeline-expandable-left";
    this._expandElement.appendChild(leftBorder);

    this._expandElement.addEventListener("click", this._onClick.bind(this));
    this._rowHeight = rowHeight;

    this._scheduleRefresh = scheduleRefresh;
}

WebInspector.TimelineRecordGraphRow.prototype = {
    update: function(record, isEven, calculator, clientWidth, expandOffset, index)
    {
        this._record = record;
        this.element.className = "timeline-graph-side timeline-category-" + record.category.name + (isEven ? " even" : "");
        var barPosition = calculator.computeBarGraphWindowPosition(record, clientWidth - expandOffset);
        this._barWithChildrenElement.style.left = barPosition.left + expandOffset + "px";
        this._barWithChildrenElement.style.width = barPosition.widthWithChildren + "px";
        this._barElement.style.left = barPosition.left + expandOffset + "px";
        this._barElement.style.width =  barPosition.width + "px";
        this._barCpuElement.style.left = barPosition.left + expandOffset + "px";
        this._barCpuElement.style.width = barPosition.cpuWidth + "px";

        if (record._visibleChildrenCount || record._invisibleChildrenCount) {
            this._expandElement.style.top = index * this._rowHeight + "px";
            this._expandElement.style.left = barPosition.left + "px";
            this._expandElement.style.width = Math.max(12, barPosition.width + 25) + "px";
            if (!record.collapsed) {
                this._expandElement.style.height = (record._visibleChildrenCount + 1) * this._rowHeight + "px";
                this._expandElement.addStyleClass("timeline-expandable-expanded");
                this._expandElement.removeStyleClass("timeline-expandable-collapsed");
            } else {
                this._expandElement.style.height = this._rowHeight + "px";
                this._expandElement.addStyleClass("timeline-expandable-collapsed");
                this._expandElement.removeStyleClass("timeline-expandable-expanded");
            }
            this._expandElement.removeStyleClass("hidden");
        } else {
            this._expandElement.addStyleClass("hidden");
        }
    },

    _onClick: function(event)
    {
        this._record.collapsed = !this._record.collapsed;
        this._scheduleRefresh();
    },

    dispose: function()
    {
        this.element.parentElement.removeChild(this.element);
        this._expandElement.parentElement.removeChild(this._expandElement);
    }
}

WebInspector.TimelinePanel.FormattedRecord = function(record, parentRecord, recordStyles, sendRequestRecords, timerRecords)
{
    var recordTypes = WebInspector.TimelineAgent.RecordType;
    var style = recordStyles[record.type];

    this.parent = parentRecord;
    if (parentRecord)
        parentRecord.children.push(this);
    this.category = style.category;
    this.title = style.title;
    this.startTime = record.startTime / 1000;
    this.data = record.data;
    this.type = record.type;
    this.endTime = (typeof record.endTime !== "undefined") ? record.endTime / 1000 : this.startTime;
    this._selfTime = this.endTime - this.startTime;
    this._lastChildEndTime = this.endTime;
    this.originalRecordForTests = record;
    this.callerScriptName = record.callerScriptName;
    this.callerScriptLine = record.callerScriptLine;
    this.totalHeapSize = record.totalHeapSize;
    this.usedHeapSize = record.usedHeapSize;

    // Make resource receive record last since request was sent; make finish record last since response received.
    if (record.type === recordTypes.ResourceSendRequest) {
        sendRequestRecords[record.data.identifier] = this;
    } else if (record.type === recordTypes.ResourceReceiveResponse) {
        var sendRequestRecord = sendRequestRecords[record.data.identifier];
        if (sendRequestRecord) { // False if we started instrumentation in the middle of request.
            record.data.url = sendRequestRecord.data.url;
            // Now that we have resource in the collection, recalculate details in order to display short url.
            sendRequestRecord.details = this._getRecordDetails(sendRequestRecord, sendRequestRecords);
        }
    } else if (record.type === recordTypes.ResourceReceiveData) {
        var sendRequestRecord = sendRequestRecords[record.data.identifier];
        if (sendRequestRecord) // False for main resource.
            record.data.url = sendRequestRecord.data.url;
    } else if (record.type === recordTypes.ResourceFinish) {
        var sendRequestRecord = sendRequestRecords[record.data.identifier];
        if (sendRequestRecord) // False for main resource.
            record.data.url = sendRequestRecord.data.url;
    } else if (record.type === recordTypes.TimerInstall) {
        this.timeout = record.data.timeout;
        this.singleShot = record.data.singleShot;
        timerRecords[record.data.timerId] = this;
    } else if (record.type === recordTypes.TimerFire) {
        var timerInstalledRecord = timerRecords[record.data.timerId];
        if (timerInstalledRecord) {
            this.callSiteScriptName = timerInstalledRecord.callerScriptName;
            this.callSiteScriptLine = timerInstalledRecord.callerScriptLine;
            this.timeout = timerInstalledRecord.timeout;
            this.singleShot = timerInstalledRecord.singleShot;
        }
    }
    this.details = this._getRecordDetails(record, sendRequestRecords);
}

WebInspector.TimelinePanel.FormattedRecord.prototype = {
    isLong: function()
    {
        return (this._lastChildEndTime - this.startTime) > WebInspector.TimelinePanel.shortRecordThreshold;
    },

    get children()
    {
        if (!this._children)
            this._children = [];
        return this._children;
    },

    _generateAggregatedInfo: function()
    {
        var cell = document.createElement("span");
        cell.className = "timeline-aggregated-info";
        for (var index in this._aggregatedStats) {
            var label = document.createElement("div");
            label.className = "timeline-aggregated-category timeline-" + index;
            cell.appendChild(label);
            var text = document.createElement("span");
            text.textContent = Number.secondsToString(this._aggregatedStats[index] + 0.0001);
            cell.appendChild(text);
        }
        return cell;
    },

    _generatePopupContent: function(calculator, categories)
    {
        var contentHelper = new WebInspector.TimelinePanel.PopupContentHelper(this.title);

        if (this._children && this._children.length) {
            contentHelper._appendTextRow("Self Time", Number.secondsToString(this._selfTime + 0.0001));
            contentHelper._appendElementRow("Aggregated Time", this._generateAggregatedInfo());
        }
        var text = Number.secondsToString(this._lastChildEndTime - this.startTime) + " (@" +
            calculator.formatValue(this.startTime - calculator.minimumBoundary) + ")";
        contentHelper._appendTextRow("Duration", text);

        const recordTypes = WebInspector.TimelineAgent.RecordType;

        switch (this.type) {
            case recordTypes.GCEvent:
                contentHelper._appendTextRow("Collected", Number.bytesToString(this.data.usedHeapSizeDelta));
                break;
            case recordTypes.TimerInstall:
            case recordTypes.TimerFire:
            case recordTypes.TimerRemove:
                contentHelper._appendTextRow("Timer Id", this.data.timerId);
                if (typeof this.timeout === "number") {
                    contentHelper._appendTextRow("Timeout", this.timeout);
                    contentHelper._appendTextRow("Repeats", !this.singleShot);
                }
                if (typeof this.callSiteScriptLine === "number")
                    contentHelper._appendLinkRow("Call Site", this.callSiteScriptName, this.callSiteScriptLine);
                break;
            case recordTypes.FunctionCall:
                contentHelper._appendLinkRow("Location", this.data.scriptName, this.data.scriptLine);
                break;
            case recordTypes.ResourceSendRequest:
            case recordTypes.ResourceReceiveResponse:
            case recordTypes.ResourceReceiveData:
            case recordTypes.ResourceFinish:
                contentHelper._appendLinkRow("Resource", this.data.url);
                if (this.data.requestMethod)
                    contentHelper._appendTextRow("Request Method", this.data.requestMethod);
                if (typeof this.data.statusCode === "number")
                    contentHelper._appendTextRow("Status Code", this.data.statusCode);
                if (this.data.mimeType)
                    contentHelper._appendTextRow("Mime Type", this.data.mimeType);
                if (typeof this.data.expectedContentLength === "number" && this.data.expectedContentLength !== -1)
                    contentHelper._appendTextRow("Expected Content Length", this.data.expectedContentLength);
                break;
            case recordTypes.EvaluateScript:
                if (this.data && this.data.url)
                    contentHelper._appendLinkRow("Script", this.data.url, this.data.lineNumber);
                break;
            case recordTypes.Paint:
                contentHelper._appendTextRow("Location", this.data.x + "\u2009\u00d7\u2009" + this.data.y);
                contentHelper._appendTextRow("Dimensions", this.data.width + "\u2009\u00d7\u2009" + this.data.height);
            default:
                if (this.details)
                    contentHelper._appendTextRow("Details", this.details);
                break;
        }

        if (this.data.scriptName && this.type !== recordTypes.FunctionCall)
            contentHelper._appendLinkRow("Function Call", this.data.scriptName, this.data.scriptLine);

        if (this.callerScriptName && this.type !== recordTypes.GCEvent)
            contentHelper._appendLinkRow("Caller", this.callerScriptName, this.callerScriptLine);

        if (this.usedHeapSize)
            contentHelper._appendTextRow("Used Heap Size", WebInspector.UIString("%s of %s", Number.bytesToString(this.usedHeapSize), Number.bytesToString(this.totalHeapSize)));

        return contentHelper._contentTable;
    },

    _getRecordDetails: function(record, sendRequestRecords)
    {
        switch (record.type) {
            case WebInspector.TimelineAgent.RecordType.GCEvent:
                return WebInspector.UIString("%s collected", Number.bytesToString(record.data.usedHeapSizeDelta));
            case WebInspector.TimelineAgent.RecordType.TimerFire:
                return record.data.scriptName ? WebInspector.linkifyResourceAsNode(record.data.scriptName, "scripts", record.data.scriptLine) : record.data.timerId;
            case WebInspector.TimelineAgent.RecordType.FunctionCall:
                return record.data.scriptName ? WebInspector.linkifyResourceAsNode(record.data.scriptName, "scripts", record.data.scriptLine) : null;
            case WebInspector.TimelineAgent.RecordType.EventDispatch:
                return record.data ? record.data.type : null;
            case WebInspector.TimelineAgent.RecordType.Paint:
                return record.data.width + "\u2009\u00d7\u2009" + record.data.height;
            case WebInspector.TimelineAgent.RecordType.TimerInstall:
            case WebInspector.TimelineAgent.RecordType.TimerRemove:
                return this.callerScriptName ? WebInspector.linkifyResourceAsNode(this.callerScriptName, "scripts", this.callerScriptLine) : record.data.timerId;
            case WebInspector.TimelineAgent.RecordType.ParseHTML:
            case WebInspector.TimelineAgent.RecordType.RecalculateStyles:
                return this.callerScriptName ? WebInspector.linkifyResourceAsNode(this.callerScriptName, "scripts", this.callerScriptLine) : null;
            case WebInspector.TimelineAgent.RecordType.EvaluateScript:
                return record.data.url ? WebInspector.linkifyResourceAsNode(record.data.url, "scripts", record.data.lineNumber) : null;
            case WebInspector.TimelineAgent.RecordType.XHRReadyStateChange:
            case WebInspector.TimelineAgent.RecordType.XHRLoad:
            case WebInspector.TimelineAgent.RecordType.ResourceSendRequest:
            case WebInspector.TimelineAgent.RecordType.ResourceReceiveData:
            case WebInspector.TimelineAgent.RecordType.ResourceReceiveResponse:
            case WebInspector.TimelineAgent.RecordType.ResourceFinish:
                return WebInspector.displayNameForURL(record.data.url);
            case WebInspector.TimelineAgent.RecordType.MarkTimeline:
                return record.data.message;
            default:
                return null;
        }
    },

    _calculateAggregatedStats: function(categories)
    {
        this._aggregatedStats = {};
        for (var category in categories)
            this._aggregatedStats[category] = 0;
        this._cpuTime = this._selfTime;

        if (this._children) {
            for (var index = this._children.length; index; --index) {
                var child = this._children[index - 1];
                this._aggregatedStats[child.category.name] += child._selfTime;
                for (var category in categories)
                    this._aggregatedStats[category] += child._aggregatedStats[category];
            }
            for (var category in this._aggregatedStats)
                this._cpuTime += this._aggregatedStats[category];
        }
    }
}

WebInspector.TimelinePanel.PopupContentHelper = function(title)
{
    this._contentTable = document.createElement("table");;
    var titleCell = this._createCell(WebInspector.UIString("%s - Details", title), "timeline-details-title");
    titleCell.colSpan = 2;
    var titleRow = document.createElement("tr");
    titleRow.appendChild(titleCell);
    this._contentTable.appendChild(titleRow);
}

WebInspector.TimelinePanel.PopupContentHelper.prototype = {
    _createCell: function(content, styleName)
    {
        var text = document.createElement("label");
        text.appendChild(document.createTextNode(content));
        var cell = document.createElement("td");
        cell.className = "timeline-details";
        if (styleName)
            cell.className += " " + styleName;
        cell.textContent = content;
        return cell;
    },

    _appendTextRow: function(title, content)
    {
        var row = document.createElement("tr");
        row.appendChild(this._createCell(WebInspector.UIString(title), "timeline-details-row-title"));
        row.appendChild(this._createCell(content, "timeline-details-row-data"));
        this._contentTable.appendChild(row);
    },

    _appendElementRow: function(title, content)
    {
        var row = document.createElement("tr");
        row.appendChild(this._createCell(WebInspector.UIString(title), "timeline-details-row-title"));
        var cell = document.createElement("td");
        cell.appendChild(content);
        row.appendChild(cell);
        this._contentTable.appendChild(row);
    },

    _appendLinkRow: function(title, scriptName, scriptLine)
    {
        var link = WebInspector.linkifyResourceAsNode(scriptName, "scripts", scriptLine, "timeline-details");
        this._appendElementRow(title, link);
    }
}